Skip to content

Release 0.4.0 (rc)#237

Merged
christophergeyer merged 31 commits into
mainfrom
rc/0.4.0
Jul 17, 2026
Merged

Release 0.4.0 (rc)#237
christophergeyer merged 31 commits into
mainfrom
rc/0.4.0

Conversation

@christophergeyer

@christophergeyer christophergeyer commented Jul 15, 2026

Copy link
Copy Markdown
Member

Draft PR to run CI on the 0.4.0 release candidate (0.3.7 batch + #228/#229 + tag series merged).

Opened as draft for CI purposes. Version bumped to 0.4.0.

🤖 Generated with Claude Code

Jon Geyer and others added 30 commits July 13, 2026 18:25
Implements `roar tag add | rm | show | history` for hereditary compliance
tags, as specified in the 2026-05-19 audit design doc (Phase 1, local-only).

Tags live under the `tag.*` key inside the existing versioned label
documents (no schema changes — the labels table already exists).  Each
kind stores a JSON array with set semantics; duplicates are silently
ignored on `add`.

New code:
  roar/core/label_constants.py  — TAG_NAMESPACE + CANONICAL_TAG_KINDS
  roar/application/tags.py      — TagService: set-accumulation over tag.*
  roar/application/query/tag.py — thin orchestration (matches label.py pattern)
  roar/application/query/requests.py — TagAdd/Rm/Show/HistoryRequest DTOs
  roar/application/query/__init__.py — lazy exports for the four tag functions
  roar/cli/commands/tag.py      — Click group with add/rm/show/history
  roar/cli/commands/__init__.py — registers `tag` in the command registry
  tests/unit/test_tag_service.py      — 25 tests for TagService logic
  tests/application/query/test_tag.py — 16 tests for query orchestration

39/39 tests pass.

Deferred: tag propagation at job-record time, --add-tag/--block-tag on
roar run, and GLaaS sync (tags publish automatically via the existing
collect_label_sync_payloads path on roar register).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes 5 ruff errors caught by CI:
  I001 (x2): unsorted import blocks
  F401: unused `call` import
  SIM117 (x2): nested with statements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the core of P2: at job-record time, tag.* values on a job's
input artifacts are unioned onto its output artifacts (set semantics,
no duplicates), so compliance tags inherit through the lineage graph
without a separate manual `roar tag add` per downstream artifact.

New: propagate_tags() in roar/application/tags.py — takes an input/output
artifact id list and a label repo, merges each input's tag.* namespace
into every output's current document. Existing output tags and non-tag
metadata are preserved; blocked_kinds lets a caller exempt specific kinds
(wired for a future --block-tag flag, not yet exposed on any command).
Writes are stamped with LABEL_ORIGIN_SYSTEM since the inheriting document
was machine-derived, not user-asserted on that specific artifact.

Wired into JobRecordingService.record_job (roar/db/services/job_recording.py):
_register_artifacts now returns the artifact ids it just linked, which
record_job passes straight to propagate_tags — avoiding a second
get_inputs/get_outputs query (which would have been redundant, since
record_job always operates on a job_id it just created).

Tests:
  tests/unit/test_tag_propagation.py            — 20 unit tests (fake label repo)
  tests/unit/test_job_recording_tag_propagation.py — 2 integration tests against
    a real SQLite-backed DatabaseContext, exercising the actual hook point

Verified no regressions: full suite failure/error counts are identical
with and without this change (116 failed / 213 errors either way, all
pre-existing environment gaps unrelated to this work); the only delta
is the 22 new passing tests above.

Deferred: --block-tag / --add-tag CLI flags on `roar run` (requires
threading two new fields through RunContext and ~5 intermediate
application/execution layers — a separate, larger change).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Completes P2 by exposing the propagation controls at record time:

  roar run --block-tag license ./relicense.sh
      Opts the "license" kind out of automatic inheritance from this
      job's inputs (e.g. a step that legitimately changes licensing).

  roar run --add-tag license=MIT --add-tag jurisdiction=EU python train.py
      Stamps extra tags directly onto this job's output artifacts at
      record time, instead of a separate `roar tag add` afterward.
      Written with user origin (same as a manual `roar tag add`),
      distinct from propagated tags which use system origin.

No GLaaS-API changes: both flags only affect what gets written locally
before the existing label-sync path (collect_label_sync_payloads /
sync_labels) picks it up — same payload shape as manual `roar tag add`.

New in roar/application/tags.py:
  - stamp_tags() — explicit KIND=VALUE tags onto outputs, user origin
  - parse_tag_kv() / parse_add_tags() — shared KIND=VALUE parsing,
    extracted from application/query/tag.py's private _parse_kv so
    `roar tag add` and `--add-tag` validate identically
  - _merge_tags_into_artifact() — shared read-modify-write merge used
    by both propagate_tags (system origin) and stamp_tags (user origin)

Plumbing: RunRequest -> run_command -> _execute_tracked_command ->
execute_and_report -> RunContext -> ExecutionJobRecorder.record ->
JobRecordingService.record_job, which now accepts block_tags/add_tags
and applies propagation (respecting blocked_kinds) then stamping.
`roar build` shares RunContext/record_job but does not expose these
flags in this change (BuildRequest untouched).

CLI validation: --add-tag values are parsed and canonical-kind-checked
via a click callback, so malformed input (e.g. `--add-tag badpair`)
fails before any execution happens, not after a partial job record.

Tests:
  tests/unit/test_tag_stamping.py — 19 tests (parse_tag_kv, parse_add_tags,
    stamp_tags merge/no-op/write-origin behavior)
  tests/unit/test_job_recording_tag_propagation.py — 3 new integration
    tests (block_tags exemption, add_tags stamping, both combined)
  tests/unit/test_run_cli_tags.py — 4 CLI-level option validation tests
  tests/application/run/test_service.py — 1 test verifying block_tags/
    add_tags reach execute_and_report from RunRequest
  tests/unit/test_job_recording.py — updated a pre-existing test's
    SimpleNamespace ctx double to include the two new RunContext fields
    it was missing (surfaced by adding ctx.block_tags/ctx.add_tags
    access in ExecutionJobRecorder.record)

Verified no regressions: full suite failure/error counts unchanged
(116 failed / 213 errors, pre-existing environment gaps) before and
after; delta is exactly the 27 new passing tests above. ruff check,
ruff format --check, and mypy roar all clean across the whole repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The tag command group was fully implemented in P1 but never added to
_COMMAND_SPECS, making `roar tag` unreachable from the CLI despite full
test coverage at the Click-group level. Also hardens the registry test
suite so a future command can't slip through the same way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Composite members (e.g. individual zarr chunk files) are pruned from a
job's plain input/output edges in favor of a bloom-filter view edge
(resolve_view_edges_for_job), so they never get a session-scoped edge
on GLaaS. Tag/label propagation was still writing tag.* documents onto
every leaf and syncing them, which 404s (`Artifact not found in
session`) since artifactBelongsToSession has no path for a view-edge
member.

resolve_view_edges_for_job now also returns the true leaf hashes
subsumed into each view edge (excluding a directly-linked anchor's own
hash), threaded through register_prepared_lineage as
composite_leaf_hashes to filter the label-sync artifact list. Ordinary
artifact registration, composite anchor registration, and sync
bookkeeping are unaffected — leaves are still registered as
content-addressed artifacts, just never handed to label sync.
Draft4 of the compliance design doc replaces draft3's bare content-hash
propagation (which contaminates: every 0-byte file in existence is one
artifact, so a tag on any empty output would apply to every empty file
in every session) with two regimes: automatic full union within a
session, and only crossing session boundaries through an explicit
human act — bind.

- Tag values move from flat string lists to provenance records
  (`{value, origin, job}`), so propagation can resolve which session a
  value belongs to.
- tag.bind holds an append-only ledger of bind/unbind events, each
  recording the (kind, value) pairs it covers. A user-origin `tag add`
  writes an implicit bind for the value it just added ("one mechanism,
  no special cases"); `--add-tag` stays session-scoped (the
  named-artifact rule).
- propagate_tags is scope-gated: a candidate value joins the union iff
  it was produced in the current session or is covered by a bind.
- New `roar tag bind`/`unbind <artifact>...` — promotes/revokes an
  artifact's current tag set, echoing what it promotes and warning on
  zero-length artifacts.
- `roar register <artifact>` implies bind of that target (named-artifact
  rule); `--bind ARTIFACT` binds specific outputs during a session-wide
  register; `--no-bind` opts out.
- tag.*/attach.* are now reserved from the generic `roar label` path,
  protecting the bind ledger's append-only integrity; TagService writes
  bypass LabelService directly (same pattern the propagation path
  already used) so its own writes aren't blocked by this.

Roar-only: no glaas-api schema or endpoint changes (still an opaque
label JSON blob, synced through the existing register/label-sync path).
GET /api/v1/labels/bound (cross-machine bind visibility) is deferred —
without it a bind made on another machine isn't visible locally, which
under-propagates (safe: missing evidence, flagged) rather than
over-propagates.
Explain how an artifact acquired a tag by walking the stored per-value
{value, origin, job} pointers and the bind ledger back to the human act
(tag add / run --add-tag), annotating any cross-session hop with the
bind that authorized it. Read-only over existing data — no schema change.

Adds TagWhyRequest, TagWhySummary/WhyNode, TagService.why, and the
`roar tag why <kind[=value]> <target>` CLI command, plus unit coverage.

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

- test_telemetry_cli: _env() cleared CI-detection vars but not the
  DO_NOT_TRACK / ROAR_NO_TELEMETRY opt-outs, so on a box where either is
  set the 5 --print/--status tests short-circuited on an inherited
  opt-out reason instead of the one under test. Neutralize both.
  (Pre-existing on main/v0.3.7 too — worth forward-porting.)
- test_register_cli_bind: a session-wide `register --bind X` now prompts
  for confirmation (#224); pass -y so the non-interactive invoke proceeds.

Full suite: 2109 passed, 10 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: roar tag functionality (CRUD, propagation, bind/unbind), rebased onto 0.3.7.
Non-canonical hereditary tag kinds were only warned about (stderr) on
`tag add` / `run --add-tag`, so a typo like 'licence' or 'contains_pil'
propagated silently as a phantom kind absent from the compliance report.

Now reject non-canonical kinds by default, extendable per project via
[tags] custom_kinds in .roarconfig (committed, team-shared). Rejection
prints a spelled-out, append-aware hint that preserves any existing
custom_kinds:

    Error: 'export_control' is not a canonical tag kind.
    hint: to allow 'export_control', update [tags] custom_kinds in .roarconfig:
    hint:     [tags]
    hint:     custom_kinds = ["data_retention", "export_control"]

Adds TagsConfig ([tags] custom_kinds), the tags.custom_kinds config key,
and roar/cli/_tag_kinds.py (resolver + hint + enforce), shared by both
enforcement points. Unit-covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`roar run --block-tag KIND=VALUE` was silently ignored — only whole-kind
`--block-tag KIND` had effect, so a relicensing step's
`--block-tag license=GPL-3.0` filtered nothing (a footgun). Now a value
barrier filters just that value from the inherited set while keeping the
rest; whole-kind blocks still drop everything, and a whole-kind block
wins over a value-level one for the same kind.

Adds parse_block_tags() + a blocked_values arg to propagate_tags; wires
it through job_recording; updates the --block-tag metavar/help.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
--block-tag / --add-tag shape the recorded tag/barrier layer but were
dropped after use, so `roar reproduce` replayed a bare `roar run <cmd>`
— re-inheriting blocked tags and dropping stamped ones, diverging the
reproduced tags from the original (bytes still matched).

Record them on the job metadata as `run_modifiers` (additive; no schema
change) at the record_job chokepoint, and have the reproduction executor
replay them (`roar run --block-tag … --add-tag … <cmd>`). The barrier is
a property of the job; this is the single source for both reproduction
and (follow-up) audit surfacing — supersedes the design's separate
tag.barrier.* label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
roar show dumped raw tag.* labels — including the internal bind ledger
(tag.bind.events=…) and provenance records ({origin,value}) — into the
generic Labels section. Now render a clean Tags: section via a shared
tag_display_pairs() (the same source roar tag show uses, so they can't
drift), keep tag.* out of the raw Labels dump, and surface a job's
recorded --block-tag modifiers as a Barriers: section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`roar tag why` only explains an artifact's tag, but `@N` resolves to a
job — a valid target for every other `roar tag` subcommand. The old
error said to "target a tracked artifact," implying the reference was
untracked, when in fact it named a perfectly tracked job. Distinguish
the job case and point at the job's output artifacts (or `tag show @N`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(rc/0.4.0): fix 6 failing tests (telemetry env hygiene + register -y)
feat(tags): add roar tag why provenance walk
feat(tags): support value-level --block-tag KIND=VALUE
feat(show): render tags cleanly + job barriers; hide the bind ledger
# Conflicts:
#	roar/cli/commands/tag.py
# Conflicts:
#	roar/db/services/job_recording.py
feat(tags): enforce canonical kinds with .roarconfig opt-in
feat(tags): record & replay roar run modifiers for reproduction
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@christophergeyer
christophergeyer marked this pull request as ready for review July 17, 2026 14:48
@christophergeyer

Copy link
Copy Markdown
Member Author

Merging 0.4.0 to main.

@christophergeyer
christophergeyer merged commit 361889b into main Jul 17, 2026
15 checks passed
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