Skip to content

Phase 1: internal package layout and its enforcement (ADR-0005) - #22

Merged
JumpTechCode merged 7 commits into
mainfrom
phase1/package-layout
Jul 26, 2026
Merged

Phase 1: internal package layout and its enforcement (ADR-0005)#22
JumpTechCode merged 7 commits into
mainfrom
phase1/package-layout

Conversation

@JumpTechCode

Copy link
Copy Markdown
Collaborator

Opens Phase 1. The ADR ships in this PR rather than ahead of it, so the record
and the mechanisms it describes land together.

What this does

  • Moves command dispatch out of package main into internal/cli, inside the
    coverage gate. Run returns an int, not an error: exit codes 70–73 report
    a verdict from a run that succeeded, so an error return would deliver a
    REGRESSED verdict as nil and map it to 0.
  • Adds internal/arch, a test-only package that reads the tree as source text
    and is default-deny — a package that appears without classifying itself fails.
  • Enables depguard, with rules generated from that same classification and
    asserted verbatim, so the linter config and the architecture test cannot drift.

Permitted imports follow from a package's class, not from a row of its own.
A row per package would make widening the star a thirty-second append that
satisfies every check; a class means widening is an edit to a rule that reads
like a decision.

Verified against the real tree, not only fixtures

Each was planted, observed, and reverted:

Planted Result
cmd/assayer importing internal/buildinfo arch test and depguard both reject, each naming the class and the ADR
internal/scratch with no classification row arch test rejects, naming the package and what to do
const defaultHarness = "claude-code" in internal/cli rejected
the same constant spelled "claudecode" rejected — an earlier segment-matching rule passed this while rejecting the hyphenated form, and claudecode is this project's own directory spelling

make verify is green: coverage 100%, 0 lint issues, 0 vulnerabilities.

What the guard cannot do, stated in the record rather than implied away

It checks topology and vocabulary. It cannot see a harness-shaped field on the
neutral representation — SidechainUUID uses no vendor word and crosses no
illegal edge. The structural rules close the generic form (no package-level
state, no init, no any-typed exported fields in the leaf, which together
also rule out a database/sql-style registry mutable at startup). The named
form needs a second, deliberately impoverished implementation in the adapter
conformance kit, recorded as a Phase 1 deliverable.

Also corrects five statements in the README that this change makes false, and
adds the missing 0004 entry to the ADR index.

JumpTechCode and others added 7 commits July 26, 2026 14:27
Phase 1 opens with three separate records — package graph, Session IR, and
machine contracts — so that revising the IR later does not force a supersede
of the document carrying the public compatibility promise. This is the first.
It merges with the code that implements it, not ahead of it.

The graph is a star, and permissions follow from a package's class rather
than from a row per package. A row per package would make widening the star
an append that satisfies every check; a class means widening is an edit to a
rule that reads like a decision. Because components may not import each
other, the root necessarily carries the program's control flow, so it splits
into one package that owns the object graph and one that owns flags, output,
and the exit-code mapping. That mapping returns an int rather than an error:
exit codes 70 to 73 report a verdict from a run that succeeded, and an error
return cannot express them.

Enforcement is depguard plus an architecture test. They are not redundant —
depguard permits by default any file no rule matches, while the test is
default-deny, so a package added without classifying itself does not build.
The test also scans source text, because the failure this exists to prevent,
comparing against a harness name in a shared package, imports nothing.

What the guard cannot do is stated in the record rather than glossed. It
checks topology and vocabulary; it cannot see a harness-shaped field on the
neutral representation. Closing that needs a second, deliberately
impoverished implementation in the conformance kit, and the record says so.

Also adds the missing 0004 entry to the index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
Run returns an int rather than an error: exit codes 70 to 73 report a verdict
from a run that succeeded, and an error return cannot express them — a
REGRESSED verdict would arrive as a nil error and be mapped to 0.

The whole 0/2/70-73 mapping now sits inside the coverage gate, which measures
internal/... only, and main is one line. The reservation comment moves with
the constants rather than being dropped in the shuffle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
Default-deny: a package that appears without a classification row fails, which
is the half depguard cannot do — depguard permits by default any file no rule
matches, so a package added in a hurry passes it unremarked.

Permitted imports follow from a package's class rather than a row of its own.
A row per package would make widening the star an append that satisfies every
check; a class means widening is an edit to allowedInternal.

Third-party imports are checked against a deliberately empty allowlist, since
the vocabulary scan catches a vendor SDK only when its module path happens to
contain a listed word. The walker uses go/parser and so ignores //go:build,
which is over-inclusive and fails safe; a fixture pins that behaviour so a
later switch to go/packages fails rather than silently narrowing the guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
The failure this prevents is import-free — comparing against a harness name in
a shared package imports nothing and satisfies any import-based rule — so the
scan reads source text rather than the import graph.

Matching is case-insensitive substring, not segment-wise. Segments were
specified first and are wrong: "claudecode" is one segment and is not equal to
"claude", so it would pass while "claude-code" fails, and "claudecode" is this
project's own directory spelling for the reference adapter. Both spellings are
now pinned by fixtures.

Every file type under the scan roots is read, not only Go source: //go:embed
is a comment and an embedded defaults file holding a harness transcript path
would otherwise sit unwatched beside core. Go comments stay unscanned, since
prose may name a harness and prose cannot branch.

internal/arch exempts itself, without which the guard flags its own word list.
The exemption count is asserted so a fourth one is a decision, not an append.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
A package-level var plus an adapter init is how an idiomatic Go registry gets
built, in the style of database/sql. Every edge in it is legal and it names no
vendor, and it makes the set of known harnesses mutable at startup — the
coupling the star was drawn to prevent. Exported fields typed any or
map[string]any are the same hole with a different shape.

Also forbids branching in the adapter registration file. That file is the only
one outside internal/adapter/** allowed to name a vendor, so it must be the
one file not allowed to make decisions, or the exemption becomes somewhere to
hide logic. A fixture pins that a real table still passes.

These fire against packages that do not exist yet, which is the point: the
check is in place before the code it constrains.

What this does not close, stated in the ADR rather than implied away: a field
named for one harness's concept uses no vendor word and crosses no illegal
edge. That needs the stub harness in the conformance kit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
One source of truth rather than set equality between two hand-maintained
copies that can both be wrong together. The test renders the block and asserts
it verbatim, so a hand edit fails and prints what to paste. Parsing the config
instead would have needed a YAML library — this module has no third-party
dependencies, and the arch class's own "imports nothing" rule would then have
been broken by the test's own import.

list-mode is lax with a deny on the module prefix. The alternatives were tried
and do not work: an empty allow list is a hard config error, strict mode denies
the standard library so every package importing fmt fails, and lax with only
an allow list enforces nothing. Adapter rules exclude test files from the broad
rule, since overlapping depguard rules combine rather than override and the
conformance-kit import would otherwise be rejected by the rule above it.

Removes the comment explaining why depguard was deferred, as it promised.

Verified both mechanisms reject independently: planting an import of
internal/buildinfo into cmd/assayer fails golangci-lint and the arch test
separately, each naming the class and the record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
Five: the ADR count, which was already wrong at four; the claim that no
working software exists, narrowed to no examining software, since the binary
still only prints its version; the project-layout block, which omitted
internal/cli and internal/arch; the sentence promising the package layout
would be recorded later; and the design-decisions list, missing 0004 and 0005.

Every relative link in the file was resolved against the tree before commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@JumpTechCode
JumpTechCode merged commit 7616a85 into main Jul 26, 2026
6 checks passed
@JumpTechCode
JumpTechCode deleted the phase1/package-layout branch July 26, 2026 20:41
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