Skip to content

refactor(ready): absorb annotation->readiness interpretation into ClassifyReadiness (links-va-001-readiness-f90.1)#202

Merged
brandon-fryslie merged 1 commit into
masterfrom
links-va-001-readiness-f90.1_issue-readiness
Jun 10, 2026
Merged

refactor(ready): absorb annotation->readiness interpretation into ClassifyReadiness (links-va-001-readiness-f90.1)#202
brandon-fryslie merged 1 commit into
masterfrom
links-va-001-readiness-f90.1_issue-readiness

Conversation

@brandon-fryslie

Copy link
Copy Markdown
Collaborator

Implements links-va-001-readiness-f90.1 (epic links-va-001-readiness-f90, F-2 Tier 1).

What

Annotations are neutral facts; readiness is an interpretation a consumer applies over them. That interpretation previously lived at six-plus sites that each re-walked annotation.Kind. This PR adds internal/cli/readiness.go with ClassifyReadiness as the single annotation→policy enforcer producing a sealed IssueReadiness whose fields mirror the three interpretation families:

  • membershipBlockingReasons() / IsReady() (len(blocking)==0 by construction; raw-slice emptiness can never become a readiness proxy)
  • stalenessIsOrphaned()
  • rank hygieneRankInversions()

Sites migrated (old per-site Kind walks deleted)

  • isReadyBlocked + readyBlockingKinds (deleted; kinds list now private to the classifier)
  • printBlockedSummary HasAny/seen/count loop → consumes []IssueReadiness + blockingKindCounts
  • printRankInversions Kind == check
  • Orphaned HasAny checks in inProgressSuffix and runOrphaned
  • dependencyIDs/buildUnblocksMap raw OpenDependency walks → DependencyIDs()
  • backlog's nonDependencyBlockingReasons → formats classified BlockingReasons
  • filterPullable (queue), pickFirstReady, sortByBlockingAnnotations, ready partition

Deliberate decision: FocusPath stays outside

sortByFocusPath keeps its direct HasAny(FocusPath) read. Focus is an ordering interpretation, readiness a membership one; routing the ordering fact through the readiness classifier would re-tangle the two concerns the focus design (#200) keeps apart. Recorded in comments at both sites.

Deviations from the epic sketch

Per [LAW:types-are-the-program] (no unread fields, strongest true theorem): no issueID field, orphaned bool instead of *InProgressAge (display derives age from UpdatedAt, not annotations), no BlockingSummary() string (the two display surfaces want different formats; formatting stays at consumers).

Verification

  • Contract test covers every annotation kind, plus empty-annotations and composite cases, plus the summary aggregation contract
  • go test ./... clean
  • Old vs new binary output on the live dogfood store is byte-identical for ready, backlog, queue

…ssifyReadiness (links-va-001-readiness-f90.1)

Annotations are neutral facts; readiness is an interpretation. That
interpretation previously lived at six-plus sites that each re-walked
annotation.Kind: isReadyBlocked, printBlockedSummary's HasAny loop over
readyBlockingKinds, printRankInversions' Kind == check, the Orphaned
HasAny checks in inProgressSuffix and runOrphaned, and the raw
OpenDependency walks in dependencyIDs/buildUnblocksMap and backlog's
nonDependencyBlockingReasons.

ClassifyReadiness (internal/cli/readiness.go) is now the single
annotation->policy enforcer producing a sealed IssueReadiness whose
fields mirror the three interpretation families: membership
(BlockingReasons), staleness (IsOrphaned), rank hygiene
(RankInversions). Every consumer reads the typed result; the per-site
Kind walks are deleted. IsReady() is len(blocking)==0 by construction,
so emptiness of the raw annotation slice can never become a readiness
proxy.

FocusPath deliberately stays OUTSIDE the classifier: it is an ordering
interpretation (sortByFocusPath), not a membership one, and routing it
through readiness would re-tangle the two concerns the focus design
keeps apart. Recorded at both sites.

Deviations from the epic sketch, per [LAW:types-are-the-program]: no
issueID field (no consumer reads it; rows carry their own identity),
orphaned bool instead of *InProgressAge (display derives age from
UpdatedAt, not from annotations), and no BlockingSummary() string (the
two display surfaces want different formats; formatting stays at the
consumers, the classifier owns only which facts block).

Verified: contract test covers every annotation kind plus the empty and
composite cases; go test ./... clean; old-vs-new binary output on the
live store is byte-identical for ready/backlog/queue.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Z.ai Coding Agent Review

Clean refactoring that consolidates scattered annotation→readiness interpretation into a single ClassifyReadiness classifier producing a typed IssueReadiness struct. No must-change items.

The PR correctly:

  • Removes readyBlockingKinds, isReadyBlocked, dependencyIDs and replaces them with one source of truth (blockingKinds + ClassifyReadiness).
  • Inverts all test assertions from isReadyBlocked (true=blocked) to IsReady() (true=ready) semantics.
  • Leaves annotation.HasAny for FocusPath in place with explicit documentation — FocusPath is ordering, not readiness.

Pre-existing note: ClassifyReadiness is called redundantly in several display paths (printInlineDeps, inProgressSuffix, printRankInversions) and inside the sort comparator in sortByBlockingAnnotations (O(n log n) calls). The function is pure and lightweight, so this isn't a LAW violation, but if annotation lists ever grow large, callers could compute readiness once and thread it through.

✅ Approved

@brandon-fryslie brandon-fryslie merged commit 83d9f91 into master Jun 10, 2026
6 checks passed
@brandon-fryslie brandon-fryslie deleted the links-va-001-readiness-f90.1_issue-readiness branch June 10, 2026 07:48
brandon-fryslie added a commit that referenced this pull request Jun 10, 2026
…recedence.First enforcer (links-va-001-precedence-jg6.1) (#207)

Three surviving helper variants (templates.firstNonEmpty,
cli.firstNonEmptySyncBranch, cli.firstNonEmptySyncRemote — the latter two
byte-identical) collapse into internal/precedence.First.

The epic's proposed trim bool is deliberately not implemented: every sync
candidate is already trimmed where it is produced, and template content
must never be trimmed — so trim is a property of the value at its
production boundary (the PathSpec precedent), not a mode of resolution.
[LAW:no-mode-explosion] [LAW:single-enforcer]

Helper-specific sync test becomes the precedence package test matrix.

The other three filed sites (config.first, workspace.firstNonEmptyTrimmed,
error_output.firstNonEmptyString) were already absorbed by #202-#204.
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