Repository navigation
feat(area-label): deterministic path sub-labels alongside the classified area - #331
Merged
Merged
Conversation
…ied area The classifier picks exactly one `area:*` and deletes every other one, so a subsystem that lives INSIDE another area's service has no way to stay filterable: it is either invisible under its parent's label, or promoted to its own area and thereby hidden FROM that parent's filter. Add an optional `sub_labels[]` block to the taxonomy. Each entry is an `area:*` name carrying `paths:` globs, matched against the PR's changed files — no model involved, because "does this PR touch X?" is a path question, not the domain-vs-path judgement call `labels[]` exists for. Matches ride alongside the classified area and are exempt from the one-area cleanup; a sub-label that stops matching is retired by that same cleanup on the next run. Names are held to the same `^area:[a-z0-9-]+$` shape and must be disjoint from `labels[]`, so every write stays inside the `area:*` namespace and no label can be both "the classification" and "an exempt sub-label". A malformed block skips the run with a warning rather than labeling under half a taxonomy, matching the existing validate_names / validate_vocab gates. sync-labels creates sub-labels from the taxonomy exactly as it does the classified ones. Opt-in and backwards compatible: a taxonomy without the key parses as an empty set and behaves exactly as before. Also adds tests/test_apply.sh — classify-and-apply.sh had no coverage, and this change lands in its label-write path. `gh` and `curl` are stubbed, with the stub `gh` holding the PR's label set in a file so POST/DELETE actually move it; the suite asserts end states (both labels applied, idempotence, stale sub-label retired, wrong area corrected without losing the sub-label, non-area labels untouched, and the fail-soft paths). CI now loops `tests/*.sh` rather than naming one file, per the repo's shell-suite convention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (10)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ELI-5
The area classifier picks exactly one
area:*label and deletes every other one. That is the right shape for "which area owns this PR?" — but it leaves a subsystem that lives inside another area's service with no way to stay filterable. Both options are bad:The fix is that it was never a one-or-the-other question. "Which area owns this?" is a judgement call, which is why a model makes it. "Does this PR touch X?" is a path question, and a path question has a deterministic answer that can ride alongside the classification instead of competing with it.
So: an optional
sub_labels:block in the taxonomy. Each entry is anarea:*name carryingpaths:globs, matched against the PR's changed files — no model, no extra API call, no extra cost. A PR touching those paths ends up with the classified area and the sub-label.What changed
scripts/area-label/lib.sh— four new pure functions, all hermetically tested:taxonomy_sub_labels— parses the optional block;[]when absent.validate_sub_labels— the gate. Names must be canonicalarea:*slugs, unique, disjoint fromlabels[], each with at least one non-blank glob.glob_to_regex— segment-aware glob → anchored regex.*/?stay within a segment,**crosses them, a leading**/matches zero or more directories. Deliberately the same semantics a workflowpaths:filter uses, so a glob can be copied between the two. Everything else that means something to a regex is escaped.matched_sub_labels— the names whose globs hit at least one changed file, in taxonomy order.scripts/area-label/classify-and-apply.sh— the end state is nowKEEP= the classified area plus every match. That one value drives all three decisions: the no-op short-circuit, the additive POST, and the cleanup's exemption list. The matched names are exempt from thearea:*cleanup; a sub-label that stops matching is not inKEEP, so the same loop retires it automatically when a PR drops those files.The sub-label POST is deliberately separate from the primary one, so the existing "add the classified area FIRST, so no failure can leave a PR unlabeled" guarantee is untouched — a hiccup adding a sub-label can never cost the PR its area.
scripts/area-label/sync-labels.sh— sub-labels are ordinary repository labels; only how they're applied differs. They're created and healed from the taxonomy exactly like the classified ones.Why the names are constrained
^area:[a-z0-9-]+$and disjoint fromlabels[]is load-bearing twice:area:*namespace. These names are handed togh label create --forceand to the label delete path, so an unconstrained name could overwrite or remove an unrelated repository label.A malformed block skips the run with a warning rather than labeling under half a taxonomy, matching the existing
validate_names/validate_vocabgates.Opt-in and backwards compatible: a taxonomy without the key parses as an empty set and behaves exactly as before. No caller change is needed to adopt it.
Tests
scripts/area-label/tests/test_apply.shis new.classify-and-apply.shhad zero coverage — the lib suite only ever exercised the pure core — and this change lands squarely in its label-writing path, which is the part that can do damage across every consuming repo.ghandcurlare stubbed, and the fakeghholds the PR's label set in a file that POST/DELETE actually mutate. That's what makes the assertions end states rather than a transcript diff:area:labels survive the cleanup untouched;sub_labels, HTTP 500) all write nothing.tests/test_lib.shgains 23 assertions over the new functions, including the glob semantics table and the disjointness gate.CI now loops
tests/*.shinstead of naming one file —bash a.sh b.shruns onlya.sh, so a listed second suite would have been silently skipped. This is the convention AGENTS.md already documents for shell suites.Non-vacuity
Mutating
matched_sub_labelsto never match kills 9 assertions intest_apply.shand 7 intest_lib.sh. Restored, both suites are green.Also green:
check_workflow_pins.py,check-org-repo-literals.sh,check_agents_md.py.A
glob_to_regexbug worth noting, since shellcheck caught it and a test now pins it: a lone backslash written as'\\'in acasepattern is two characters and can never match a one-character input, so a literal\in a glob would have gone unescaped into the regex.Docs
scripts/area-label/README.md,docs/callers/pr-area-label.md(including the glob semantics and the honest limits — matching is paths-only, and the changed-file list comes fromgh pr view --json files, which caps at 100), the README catalog row, and the reusable's header contract.🤖 Generated with Claude Code