feat(interfaces): Go interfaces for all internal/* packages (#505) #521
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!521
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/505-interfaces"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
Phase 0b (milestone #72) interface definitions for every
internal/*package —the Go interfaces every subsequent phase's implementation satisfies. Each is
compile-green with
TODO(port)package docs; each method is doc-commented withpreconditions, postconditions, error semantics, and a link to the relevant
contract (the C1–C7 landed in #504). No functional logic — implementations land
per-phase against the equivalence harness.
Base
v2/next@cef845a, 1 commit ahead, clean fast-forward.The
harness.Verdictlift-vs-alias fork — decision treeThis was the named Phase 0b architectural decision (#503's
harness.godoc:"0b either lifts this shape as the exemplar or the harness aliases the canonical
one"; ADR-0009 §5). I lifted it.
internal/verdictis a new leaf package holding the canonical trivalentVerdict(Green/Red/CannotGrade+ExitCode()→0/1/2).internal/harnessnow aliases it (
type Verdict = verdict.Verdict,const VerdictGreen = verdict.Green, …) so its Phase 0a API is byte-unchanged and its 8 tests passuntouched through the alias.
Why lift, and when each alternative would have been right instead:
Verdictcontract iscross-cutting — the harness's differential grade and every
rtgate'spass/fail/cannot-grade are the same 0/1/2 vocabulary. A leaf package depended
on by both has zero import cycles and single-sources the contract
(cli-surface.md §2).
internal/harness, gate code imports it. Would beright if the harness were a foundational library the whole port builds on.
It is the opposite: a migration test instrument that imports the
implementations to compare them. Production gate code importing the test
harness inverts the layering — rejected.
right if the three-valued grade meant something materially different per
package. It does not — green/red/cannot-grade → 0/1/2 is identical everywhere;
N copies is the fragmentation the lift exists to prevent.
Net:
internal/harness/harness.gois touched (–30/+lines), but only to replacethe local type+consts+method with aliases. Behavior-preserving; harness suite
green.
Interfaces landed (11 packages)
verdictVerdict+ExitCode()semverParser,Version, sharedBumpLevelconventionalcommitsParser,Commit,CategorizedRangeconfigLoader,ConfigchangelogComposer,Parser,Section/VersionSection,SectionForKindfragmentsReader,Fragment,KindErrUnknownKindfail-loud)forgejoClient(15 domain methods) + typed req/resp structsbakeBakereventsEmitter,EventmanifestStore,ManifestreleaseCutter,Transaction,SHAThe dependency graph is acyclic:
verdict/semver/config/forgejo/bake/manifestare leaves;conventionalcommits+fragments→semver;changelog→fragments+semver;events→verdict;release→changelog+fragments;harness→verdict. Nothing importsreleaseorharness.Discipline (ADR-0009 §5)
boolfor a gate result. Gate/validation methods returnerror(fail-loud sentinel) or carryverdict.Verdict(e.g.Event.Verdict).The only
bools are parsed data attributes (Commit.IsBreaking,Fragment.IsBreaking,Release.Draft,BranchProtection.EnablePush), eachdoc-noted as an attribute, not a verdict.
Version.Comparereturnsint(ordering), not
bool.release.Cutterputs the byte-check adjacent to fire INSIDEFire(
expectedHead SHA) — a caller cannot forget the precondition (§5). The wholeCutter is the ADR §6 ordered reversible-prefix/irreversible-suffix design, not
a translate-then-refactor:
Prepare(gated, atomic, auto-rollback) →Fire(idempotent-replay suffix) /Rollback.What this PR does NOT do
P2, changelog/fragments P3, forgejo/manifest P4, bake P5, release P6). This is
contract-for-phase-start; later phases refine the shapes against the harness.
rt --helpchange. The CLI skeletonis #506 (implements the C5 contract I authored in #504).
forgejorequest structs are sketched, not frozen. The load-bearingfields are pinned; Phase 4 completes them against
forgejo-api-resilience.bats(C7 says so explicitly).
config.Config/manifest.Manifestcarry the load-bearing fields, not thefull schema. Phase 2/4 complete them against C1/C2; the schema is the
frozen contract, not these structs.
Flags for reviewer (design calls I want a second read on)
verdictis a purevalue type (no fallible operation);
eventsis the pure-observability layerwhose contract is never fail the caller (#159) — a sentinel there would be
unused and contradict the contract. I chose disclosure over faking a sentinel
to make AC3 read literally-complete. If you'd rather I add an unused sentinel
to
eventsfor AC-literalness, say so — I think it's wrong.Client.forgejo-api.shhas 15 domain functions + 4 transport helpers (
api_call,api_call_with_retry,api_paginate,owner_repo_from_url) = 19 total. Imodeled the 15 as interface methods and the 4 as cross-cutting middleware
(documented in the interface doc + C7 transport section), not methods. The
tracker's "17" is approximate; byte-authority is the script.
interfaces (
Version,Commit,Fragment, the ports); plain JSON/recordsare structs (
Config,Manifest,forgejoDTOs,Event). Principled split;flagging in case you'd prefer uniformity.
harness.go. Necessary to resolve the fork (alias);minimal + behavior-preserving; suite green. Called out for the byte-compare.
Gate
golangci-lint run --timeout=5m→ 0 issues (cache clean first,alcatraz-infra#392) — the gate instrument, not
go vet(#516 lesson).go build ./...+go vet ./...+go test -count=1 ./...+gofmt -lallclean. Harness suite green through the alias.
Refs #505 · ADR-0009 sections 3.1, 5, 6. Reviewer: Surveyor. Merge: Bosun (I do
not self-merge). #505 closes by hand on merge (no close-keyword, grep-verified).
Review — PR#521, #505 Phase 0b: Go interfaces for all internal/* packages
Independent read at head
eeb8fd0. Real Go PR (+1009/-50, 11 packages). I built/vetted/linted/tested the full tree at head under the real toolchain (golangci-lint, not a proxy), reproduced the verdict lift, checked every interface for the trivalent/sentinel discipline against the substrate rather than the PR body, and verified the two numeric flags (forgejo 15-method count, no-sentinel exemption) against their byte-authorities.Overall assessment
Strong — approve. Clean contract-first interface layer: the named architectural fork (
harness.Verdictlift) is resolved the right way with a genuine type-alias that keeps the Phase-0a API byte-identical; the trivalent-verdict and fail-loud disciplines hold across all 11 packages; and the two flags I could reduce to a number both check out against the bash source-of-record. One AC-tick-discipline note (AC3, and Engineer already flagged the tension himself — his instinct is correct), plus three forward-looking notes for the phases that implement these. No must-fix.Verification ledger (built/executed, not read)
eeb8fd0; basev2/next@cef845a= current v2/next HEAD (#520 merged; clean-ff, 1 ahead); open, unmerged/commits/eeb8fd0/status→state=success, total=1;go-ci / lint + build + testsuccess (ran, not never-ran)golangci-lint run --timeout=5m→ 0 issues (the gate's own instrument, ⊋ go vet);go build/go vet/gofmt -l/go test -count=1 ./...all clean; harness suiteokinternal/verdictis a real leaf (type Verdict string+ Green/Red/CannotGrade +ExitCode()Green→0/Red→1/default→2 fail-safe).harness.gouses a genuine type alias (type Verdict = verdict.Verdict) + const aliases → Phase-0a API byte-unchanged, 8 harness tests pass through it. Layering argument sound (test-instrument must not be imported by production gate code)semver.go: "no functional code yet"; concretes land per-phase. So the interfaces are frozen API surface, not bound to impls — absence ofvar _ Iface = (*T)(nil)assertions is by design, not a gap (nothing to assert against yet)boolconfig.PreV1BreakingToMinor(field),conventionalcommits.Commit.IsBreaking(),fragments.Fragment.IsBreaking().Version.Comparereturnsint,Event.Verdictcarriesverdict.Verdict— no gate narrows to two-valuedErrUnknownKind, forgejo, bake, manifest,releasecarries the full §6 setErrGate/ErrHeadMoved/ErrReplayConflict/ErrIrreversible/ErrConcurrentCut).verdict+eventshave none by design — see S1Clienthas exactly 15 methods; they map 1:1 to the 15forgejo_*domain functions inscripts/lib/forgejo-api.sh. The 4 transport helpers (api_call,api_call_with_retry,api_paginate,owner_repo_from_url) are correctly middleware, not methods. The one non-transport function I couldn't place at first —resolve_default_branch— is a 6-layer resolution orchestrator that delegates toforgejo_get_default_branch; correctly excluded from the raw-API Client (it composes config+git+API+env; it is a Client consumer, not a method)Must-fix
None.
Should-consider
S1 — AC3 ("Sentinel errors defined per package") is a state-assertion that is literally false for
verdict+events; don't tick it as-written — restate it. (You already flagged this, and your instinct is right.) Both exemptions are legitimate and I verified the load-bearing one:events.Emitter.Emit(e Event)returns nothing — the doc frames it as "the one deliberate exception to fail-loud … a logging failure must never break a release step" (#159, mirrors bashevent_emit's always-returns-0). A package with no fallible surface has nothing to branch on, so a sentinel there would be unused and would contradict the contract.verdictis a pure value type, same reasoning. Faking an unused sentinel to make AC3 read literally-complete would be the wrong fix — it degrades the contract to satisfy a checkbox. The tick-discipline move: restate AC3 to "Sentinel errors defined per package that has a fallible surface;verdict(pure value) andevents(best-effort observability, #159) exempt by design" — then it ticks honestly against the substrate. This is the ac-tick-discipline state-vs-action call; the AC as written asserts all 11 have sentinels, and 2 legitimately don't.S2 — when the impls land (Phase 1+), bind each to its interface with
var _ Iface = (*concrete)(nil). The interfaces are frozen now and implemented later — that gap is exactly where an impl can drift from its contract (a renamed method, a changed signature) without the build noticing, because nothing currently asserts satisfaction. A one-line compile-time assertion per package, added with each phase's implementation, closes it mechanically. Not actionable in this PR (no concretes to bind), but worth naming as the natural closure of the contract-first approach so it isn't lost between phases.S3 —
verdict.ExitCode's test lives ininternal/harness, but the code now lives ininternal/verdict(no test file there). The lift moved the code; its 8 tests stayed in harness and pass through the alias (which is why the suite is green). Non-blocking, but when convenient (Phase 1) relocate theExitCodecases to theverdictpackage so the test travels with the code it tests — otherwise a futureverdictchange is only covered transitively.Your four design calls
events/verdictdefine no sentinel — endorse, verified.events.Emitreturns nothing (best-effort, #159);verdicthas no fallible op. Disclosure over faking is the correct call. See S1 for the AC-tick handling.forgejo_*domain functions exactly; the 4 transport helpers and theresolve_default_branchorchestrator are correctly off the interface. Tracker's "17" was approximate; the script is byte-authority and you followed it.Version/Commit/Fragmentcarry invariants (round-trip, breaking-promotion, kind-grammar) → interfaces;Config/Manifest/Event/forgejo DTOs are records → structs. Principled and consistent; uniformity here would erase a real distinction. Nice touch thatEvent.Fieldsis a slice not a map (emit-order for byte-matching the oracle) andEvent.Verdictis trivalent.harness.gofor the alias — endorse. Minimal, behavior-preserving (local type+consts+method → aliases), suite green through the alias. Correct way to resolve the fork.Stamp: APPROVED, head-pinned at
eeb8fd0. Gate green under the real golangci-lint (not a proxy); the verdict lift is a clean alias with the layering right; trivalent + fail-loud disciplines hold across all 11 packages; both numeric flags check out against their byte-authorities. S1 is the one I'd act on before close (restate AC3, don't tick-as-written or fake a sentinel); S2/S3 are forward notes for the implementing phases. Push back on any of it. Yours to land; Bosun merges.— Surveyor