Audit the public facade boundary

This commit is contained in:
2026-09-12 16:26:44 +00:00
parent 7f46c947dd
commit 5dc7e18d83
9 changed files with 216 additions and 136 deletions

View File

@@ -1,11 +1,11 @@
{ {
"schema_version": 2, "schema_version": 2,
"commit": "550f92d7c6f197d7f4996919a418d9ca0174eb07", "commit": "7f46c947dd56f437077df475ba5bba8fd864e2e1",
"indexed_at": "2026-09-12T16:21:16Z", "indexed_at": "2026-09-12T16:26:42Z",
"project": "home-eric-Workspace-ConfigKit", "project": "home-eric-Workspace-ConfigKit",
"nodes": 1625, "nodes": 1625,
"edges": 7835, "edges": 7824,
"original_size": 8126464, "original_size": 8126464,
"compressed_size": 1330563, "compressed_size": 1329933,
"compression_level": 3 "compression_level": 3
} }

Binary file not shown.

View File

@@ -2,7 +2,7 @@
## Status ## Status
Accepted. This records a design decision, not implementation completion. Accepted and implemented on 2026-09-12.
## Date ## Date
@@ -10,32 +10,31 @@ Accepted. This records a design decision, not implementation completion.
## Context ## Context
The root `configkit` package currently combines the supported consumer API with The root `configkit` package formerly combined the supported consumer API with
the loading, decoding, inspection, and rendering machinery. That makes ordinary loading, decoding, inspection, and rendering machinery. That made ordinary
consumer navigation harder and risks exposing implementation details through consumer navigation harder and risked exposing implementation details through
future public declarations. Moving files alone would not preserve the identity, future public declarations. Moving files alone would not preserve the identity,
method sets, callbacks, ownership rules, or reflection-sensitive behavior of method sets, callbacks, ownership rules, or reflection-sensitive behavior of the
the root's public types. root's public types.
## Decision ## Decision
Keep `configkit` as the sole supported consumer import path and keep all public Keep `configkit` as the sole supported consumer import path and keep all public
named types defined there. When lifecycle machinery is extracted, it will live named types defined there. Lifecycle machinery lives in a cohesive
in a cohesive `internal/engine`; the root facade may hold private engine state `internal/engine`; the root facade holds private engine state and forwards
and forward deliberately selected methods. The dependency direction is root to deliberately selected methods. The dependency direction is root to engine and
engine and its supporting internal packages. Internal production packages do its supporting internal packages. Internal production packages do not import the
not import the root package. root package.
The facade will use root-owned wrappers and explicit private conversions. It The facade will use root-owned wrappers and explicit private conversions. It
will not use public aliases to engine types, anonymous embedding that promotes will not use public aliases to engine types, anonymous embedding that promotes
engine methods, public engine types in signatures or fields, or public bridge engine methods, public engine types in signatures or fields, or public bridge
methods. Existing document and YAML boundaries remain intact. methods. Existing document and YAML boundaries remain intact.
The migration may use a temporary internal candidate facade while the unchanged The migration used a temporary internal candidate facade while the unchanged root
root remains the only production entry point. That duplication is a bounded remained the only production entry point. That duplication was a bounded
relocation mechanism, not a second supported package, feature path, or runtime relocation mechanism, not a second supported package, feature path, or runtime
selection mechanism. The final cutover removes the temporary candidate and old selection mechanism; the final cutover removed it and the old root implementation.
root implementation together.
The [architecture policy](../policy/architecture.md#public-api-and-dependency-direction) The [architecture policy](../policy/architecture.md#public-api-and-dependency-direction)
owns the binding boundary. The [public-facade roadmap](../roadmap/public.md) owns the binding boundary. The [public-facade roadmap](../roadmap/public.md)

View File

@@ -98,15 +98,15 @@ code changes. Use the component overview to find the corresponding implementatio
| Task | Behavioral test owners | Focused command | | Task | Behavioral test owners | Focused command |
| --- | --- | --- | | --- | --- | --- |
| YAML syntax, diagnostics, parser limits | [Adapter tests](../internal/yaml/parse_test.go), [source translation](../internal/engine/parse_test.go) | `go test ./internal/yaml` and `go test . -run '^TestParseSource'` | | YAML syntax, diagnostics, parser limits | [Adapter tests](../internal/yaml/parse_test.go), [source translation](../internal/engine/parse_test.go) | `go test ./internal/yaml` and `go test ./internal/engine -run '^TestParseSource'` |
| File/reader access, imports, globs, containment | [Sources](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [imports](../internal/engine/imports_test.go), [globs](../internal/engine/glob_test.go) | `go test . -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` | | File/reader access, imports, globs, containment | [Sources](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [imports](../internal/engine/imports_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
| Composition, profiles, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [profiles](../internal/engine/profile_test.go), [overrides](../internal/engine/override_test.go) | `go test . -run 'Compose\|Overlay\|Profile\|Override'` | | Composition, profiles, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [profiles](../internal/engine/profile_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Profile\|Override'` |
| Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test . -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` | | Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
| Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test . -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` | | Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` |
| Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test . -run 'Path\|Metadata\|Provenance\|Queries'` | | Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` |
| Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test . -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` | | Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test ./internal/engine -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` |
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` | | Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` |
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go) | `go test . -run '^(TestLocalDocumentationLinks\|TestDependencyImportBoundary)$'` | | Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go) | `go test . -run 'TestLocalDocumentationLinks|TestDependencyImportBoundary|TestPublicFacadeBoundary'` |
| Check-command behavior | [Shell regression](../release_check_test.go) | `go test . -run '^TestCheckScripts'` | | Check-command behavior | [Shell regression](../release_check_test.go) | `go test . -run '^TestCheckScripts'` |
## Fuzz validation ## Fuzz validation
@@ -115,7 +115,20 @@ Run the bounded fuzz targets independently from the repository root. These exact
commands keep each campaign short and constrain concurrent workers: commands keep each campaign short and constrain concurrent workers:
```sh ```sh
go test . -run='^$' -fuzz='^FuzzParsePath$' -fuzztime=30s -parallel=2 go test ./internal/engine -run='^$' -fuzz='^FuzzParsePath$' -fuzztime=30s -parallel=2
go test ./internal/yaml -run='^$' -fuzz='^FuzzParseDocument$' -fuzztime=30s -parallel=2 go test ./internal/yaml -run='^$' -fuzz='^FuzzParseDocument$' -fuzztime=30s -parallel=2
go test . -run='^$' -fuzz='^FuzzCompositionAndOverlay$' -fuzztime=30s -parallel=2 go test ./internal/engine -run='^$' -fuzz='^FuzzCompositionAndOverlay$' -fuzztime=30s -parallel=2
```
` |
| Check-command behavior | [Shell regression](../release_check_test.go) | `go test . -run '^TestCheckScripts'` |
## Fuzz validation
Run the bounded fuzz targets independently from the repository root. These exact
commands keep each campaign short and constrain concurrent workers:
```sh
go test ./internal/engine -run='^$' -fuzz='^FuzzParsePath$' -fuzztime=30s -parallel=2
go test ./internal/yaml -run='^$' -fuzz='^FuzzParseDocument$' -fuzztime=30s -parallel=2
go test ./internal/engine -run='^$' -fuzz='^FuzzCompositionAndOverlay$' -fuzztime=30s -parallel=2
``` ```

View File

@@ -1,94 +1,39 @@
# Internal Component Overview # Internal Component Overview
ConfigKit exposes the root `configkit` package and keeps its document and YAML ConfigKit exposes one supported root `configkit` package. The root owns public
representations private. The implemented components and their owners are: named types, declaration comments, constructors, and explicit conversion/wrapper
code. It delegates loading and frozen inspection to `internal/engine` through
private state; public signatures and fields do not expose engine types.
- a temporary `internal/engine` relocation candidate containing a mechanical Implemented component owners:
copy of the root loading, decoding, resolution, provenance, inspection, and
rendering mechanisms with their behavior tests; it imports no root code and
is not yet used by the root package or consumers;
- immutable semantic `Path` construction and strict JSON Pointer parsing;
- detached effective presence/origin and chronological history queries in
`metadata.go`, with provenance and diagnostic record types;
- canonical, control-safe diagnostic rendering and structured `LoadError` access;
- opaque file, byte, reader, and no-input source construction in `source.go`,
with shared regular-file checks and bounded read mechanics in `source_file.go`;
- a parser-independent ordered tree in `internal/document`, with root-private
semantic lookup, indexing, and value traversal in `document_walk.go`;
- the sole upstream YAML integration in `internal/yaml`, which translates validated
input into that owned tree and exposes no parser types outside its boundary;
`parse.go` owns shared translation into detached, ordered public diagnostics;
- a private strict composer in `compose.go` that joins only disjoint document-tree
definitions, retains ordered claims and composite mapping origins, reports every
conflicting location, and never aliases an input tree;
- fixed selected-profile loading in `profile.go` and recursive, non-aliasing overlay
with displacement records in `overlay.go`; overlay records retain origins,
while value explanation uses separate history applications;
- CLI override validation/preparation in `override.go`, parser-neutral values in
`override_value.go`, and synthetic-source application in `override_apply.go`,
sharing overlay, source, byte, node, and depth policies;
- a per-operation private reflection descriptor graph in `schema.go` for supported
types, YAML field binding, inlining, secret tags, recursion, and semantic paths;
- descriptor-driven canonical projection in `projection.go`, detached typed
cloning with explicit secret-state modes in `clone.go`, shared reflection and
cycle/limit state in `value_operation.go`, and configuration-value comparison
in `document_compare.go`; validation separately audits sensitivity and secrets;
- strict schema-driven decoding over detached defaults in `decode.go`, including
mapping/sequence semantics, null and numeric policy, safe codec errors, presence,
baseline/final trees, and initial default/input origins;
- public lifecycle coordination in `load.go`, option selection in `options.go`,
input assembly in `load_input.go`, typed phases/publication in `load_result.go`,
bounded canonical import DFS in `imports.go`, filesystem context/containment in
`filesystem.go`, directive parsing/restrictions in `directives.go`,
pattern-directed glob expansion in `glob.go`, schema-version checking and report
contexts in `validation.go`, and layered metadata/history assembly in
`load_metadata.go`;
- recorded application defaulting and normalization in `resolution.go`, including
callback-scoped fixed entry metadata/source queries, explicitness queries, helper
verification, structural mutation audit, coalesced intent, private before/after
snapshots, and resolution metadata integration;
- post-resolution schema-aware report collection and validator mutation detection in
`validation.go`, completing the public callback lifecycle after composed input;
- opaque typed environment references and per-load name resolution in
`secret_reference.go`, including credential-free projection/cloning and private
validator-state auditing;
- explicit bounded startup environment bootstrap and safe partial reporting in
`secrets.go`;
- fixed-point tag/predicate/dependency sensitivity classification in `secret.go`;
- canonical effective output in `render.go`, using frozen-snapshot redaction
shared with explanation in `snapshot.go`;
- detached public explanation records and coordination in `explain.go`, immutable
input applications/replay and retained-node budgeting in `history.go`, shared
safe snapshot projection in `snapshot.go`, and deterministic human output in
`explain_render.go`; the only upstream dumper integration remains isolated in
`internal/yaml/write.go`;
- a separate maintained `examples/consumers` package with four sanitized consumer
shapes, fixtures, domain validators, and runtime-boundary functions; and
- an everyday single-toolchain check in `scripts/check.sh` and a stricter
source-candidate checker in `scripts/release-check.sh`, with local
documentation-link and public import-boundary gates owned by root-package tests.
Dependency direction remains inward: the public lifecycle coordinates private - `internal/engine` owns source acquisition, composition, schema inspection,
source, document, schema, and inspection helpers. Semantic paths stay in the root decoding, lifecycle coordination, callback auditing, provenance/history,
package; `internal/document` owns only representation and cloning and never imports secret resolution and bootstrap, and safe inspection rendering. It never
the root package. Only `internal/yaml` imports the upstream YAML package. Maintained imports the root package.
consumer examples use the public package and standard library. - Root `Source`, `Path`, `Secret`, records, lifecycle callbacks, results, and
bootstrap reports are root-defined wrappers or records. Callback wrappers pass
only live engine context during a callback; `Result.Config` is detached and its
private inspection remains frozen independently.
- Root conversion files explicitly cross the path, record, callback, secret, and
error boundaries. There are no public aliases, anonymous engine embeddings, or
public engine bridge methods.
- `internal/document` owns the parser-independent ordered tree and cloning.
`internal/yaml` is the sole upstream YAML adapter and parser/dumper boundary.
- Engine mechanism tests own source, composition, decoding, callback, provenance,
secret, history, rendering, and bootstrap behavior. Root tests own public API
compatibility, facade integration, documentation links, and dependency/public
boundary checks. Consumer examples remain under `examples/consumers`.
The [configuration contract](../config.md) owns implemented input, schema, decoding, Dependency direction is root to engine to document/YAML support. The root may
loading lifecycle, path imports, composition, profile/CLI overlay, version checking, import only `internal/engine` among project internal production packages; engine
recorded resolution, typed secret references/bootstrap, semantic validation, and its supporting packages do not import root. The root boundary test enforces
metadata, secret classification, redacted effective rendering, historical these constraints and rejects exported aliases, fields, or signatures involving
explanation, and ownership behavior. internal types.
The [consumer adoption guide](../consumers/README.md) owns current application-fit
and migration guidance.
The source-extraction inventory records the application foundations used by the The [configuration contract](../config.md) owns implemented YAML and lifecycle
implementation: semantics. The [consumer adoption guide](../consumers/README.md) owns application
guidance. The [architecture policy](../policy/architecture.md) owns contributor
- [Extraction inventory](extraction.md) boundaries, and [ADR 0006](../adr/0006-public-facade.md) records the facade
decision. See the [development guide](../development.md) for validation commands
The [architecture policy](../policy/architecture.md) owns binding boundaries and and [release procedure](../release.md) for source-candidate gates.
invariants. The [deferred scope](../roadmap/deferred.md) records deliberate
exclusions. See the
[development guide](../development.md) for contributor orientation and validation
and the [release procedure](../release.md) for source-candidate gates.

View File

@@ -83,10 +83,10 @@ types or acquire internal methods through anonymous embedding. Root production
code may depend inward on a cohesive engine and its supporting internal packages, code may depend inward on a cohesive engine and its supporting internal packages,
but internal production packages must not import the root. Explicit forwarding but internal production packages must not import the root. Explicit forwarding
and local conversion are acceptable where they preserve type identity, and local conversion are acceptable where they preserve type identity,
ownership, callback lifetime, and inspection behavior. The accepted ownership, callback lifetime, and inspection behavior. The accepted root-wrapper
root-wrapper boundary and its migration rationale are recorded in boundary and its migration rationale are recorded in
[ADR 0006](../adr/0006-public-facade.md); its implementation remains tracked by [ADR 0006](../adr/0006-public-facade.md); completed implementation evidence is
the public-facade roadmap. recorded in the public-facade roadmap.
Separate input access, document processing, composition, decoding, metadata, and Separate input access, document processing, composition, decoding, metadata, and
rendering responsibilities without prescribing an interface or package for each. rendering responsibilities without prescribing an interface or package for each.

View File

@@ -1,8 +1,8 @@
# Public facade: implementation plan # Public facade: implementation plan
Status: not started. Baseline: `28e7fc5f690b36c60e4d03586afcff00fb1dee90`. Status: complete (2026-09-12). Baseline: `28e7fc5f690b36c60e4d03586afcff00fb1dee90`.
The [feature roadmap](public.md) owns the target architecture and user intent. The [feature roadmap](public.md) owns the implemented architecture and user intent.
This plan owns implementation decisions, ordered work, validation, and handoffs. This completed plan records implementation decisions, ordered work, validation, and handoffs.
Each numbered stage is sized for one gpt-5.6-terra coding-agent assignment; Each numbered stage is sized for one gpt-5.6-terra coding-agent assignment;
no stage requires subagents or changes to downstream repositories. no stage requires subagents or changes to downstream repositories.
@@ -508,7 +508,7 @@ boundary and brings documentation in line with the completed architecture.
## Stage 9 — Audit the public boundary and finalize documentation ## Stage 9 — Audit the public boundary and finalize documentation
Status: not started. Status: complete (2026-09-12).
1. Compare the final complete exported API against the baseline. Audit public 1. Compare the final complete exported API against the baseline. Audit public
signatures, fields, aliases, embedded types, method sets, and error returns signatures, fields, aliases, embedded types, method sets, and error returns
@@ -538,6 +538,29 @@ supported baseline and newer Go toolchains recorded in the release procedure.
Record actual versions/platforms; do not treat Linux tests or cross-builds as Record actual versions/platforms; do not treat Linux tests or cross-builds as
native macOS release evidence. Do not publish or select a version in this stage. native macOS release evidence. Do not publish or select a version in this stage.
### Completion
Audited the final root facade against the established public surface and added a
root AST boundary check for exported aliases, fields, and signatures involving
internal types. The dependency boundary check now also rejects root production
imports that bypass the engine and internal production imports of the root.
The completed root retains only facade wrappers, records, conversions, and
entry-point delegation; engine mechanism tests own lifecycle behavior.
Updated the architecture policy, ADR status, component overview, development
test/fuzz navigation, and public-facade roadmap to describe the implemented
layout. The roadmap is now a completed implementation record rather than a
future plan. Root facade integration and engine tests cover source ownership,
exact-type secret binding, callback activity/audit, structured errors, detached
configuration versus frozen inspection, and resolution entry snapshots.
Linux validation passed with Go 1.25.5 and Go 1.26.7 through
`CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.5 CONFIGKIT_NEWER_TOOLCHAIN=go1.26.7 ./scripts/release-check.sh`.
It also passed the everyday check, full race suite, consumer examples, and local
documentation/import/public-boundary tests. This is not native macOS evidence
and no release version, tag, or publication was selected. There were no
deviations or remaining risks.
## Open Questions ## Open Questions
None requiring a user decision were identified. Wrappers, public type ownership, None requiring a user decision were identified. Wrappers, public type ownership,

View File

@@ -1,8 +1,8 @@
# Public facade and internal implementation # Public facade and internal implementation
Status: agreed direction; not implemented. This roadmap defines the desired end Status: implemented on 2026-09-12. This roadmap records the intended architecture
state and user intent. The [implementation plan](implementation.md) owns the and user intent; the completed [implementation plan](implementation.md) records
ordered work, concrete adapter design, validation commands, and handoffs. the ordered work, validation, and handoffs.
## Intent ## Intent
@@ -31,13 +31,13 @@ design. Existing [configuration contracts](../config.md) and exported Go
declaration comments remain authoritative. The user-approved wrapper boundary is declaration comments remain authoritative. The user-approved wrapper boundary is
a contributor design choice, not a strengthening of consumer behavior contracts. a contributor design choice, not a strengthening of consumer behavior contracts.
## Current architecture ## Implemented architecture
The root currently owns both public declarations and most private implementation: The root owns public declarations, records, wrappers, and deliberate forwarding.
source acquisition, imports, composition and overlays, reflection descriptors, `internal/engine` owns source acquisition, imports, composition and overlays,
decoding, resolution and validation coordination, provenance, history, secret reflection descriptors, decoding, resolution and validation coordination,
handling, and rendering. The [component overview](../internal/overview.md) owns provenance, history, secret handling, and rendering. The
the detailed inventory. [component overview](../internal/overview.md) owns the detailed inventory.
Two established internal boundaries already exist: `internal/document` owns the Two established internal boundaries already exist: `internal/document` owns the
parser-independent tree, and `internal/yaml` owns upstream YAML integration. parser-independent tree, and `internal/yaml` owns upstream YAML integration.
@@ -49,7 +49,7 @@ callback state; `Path` holds semantic segments; and `Secret` holds private
reference and resolved-value state. Moving implementation requires deliberate reference and resolved-value state. Moving implementation requires deliberate
type ownership, not just file relocation. type ownership, not just file relocation.
## Target architecture ## Architecture
### Public root package ### Public root package
@@ -140,7 +140,7 @@ need to change an established contract requires a separate explicit decision.
Version selection and publication remain governed by the Version selection and publication remain governed by the
[release policy](../policy/release.md), not by this roadmap. [release policy](../policy/release.md), not by this roadmap.
## Completion criteria ## Completion evidence
Completion means the root is a legible public surface, substantive plumbing is Completion means the root is a legible public surface, substantive plumbing is
internally owned, consumers require no migration, and representative internally owned, consumers require no migration, and representative

View File

@@ -1,6 +1,7 @@
package configkit package configkit
import ( import (
"go/ast"
"go/parser" "go/parser"
"go/token" "go/token"
"io/fs" "io/fs"
@@ -8,6 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"unicode"
) )
func TestDependencyImportBoundary(t *testing.T) { func TestDependencyImportBoundary(t *testing.T) {
@@ -41,6 +43,15 @@ func TestDependencyImportBoundary(t *testing.T) {
if strings.HasPrefix(importPath, "github.com/google/go-cmp/") && !strings.HasSuffix(path, "_test.go") { if strings.HasPrefix(importPath, "github.com/google/go-cmp/") && !strings.HasSuffix(path, "_test.go") {
t.Errorf("%s imports test-only dependency %s", path, importPath) t.Errorf("%s imports test-only dependency %s", path, importPath)
} }
production := !strings.HasSuffix(path, "_test.go")
rootProduction := production && filepath.Dir(path) == "."
internalProduction := production && strings.HasPrefix(filepath.ToSlash(path), "internal/")
if rootProduction && strings.HasPrefix(importPath, "gitea.maximumdirect.net/eric/ConfigKit/internal/") && importPath != "gitea.maximumdirect.net/eric/ConfigKit/internal/engine" {
t.Errorf("%s bypasses the engine boundary through %s", path, importPath)
}
if internalProduction && importPath == "gitea.maximumdirect.net/eric/ConfigKit" {
t.Errorf("%s imports the public root package", path)
}
} }
return nil return nil
}) })
@@ -48,3 +59,92 @@ func TestDependencyImportBoundary(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestPublicFacadeBoundary(t *testing.T) {
t.Parallel()
entries, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
for _, path := range entries {
if strings.HasSuffix(path, "_test.go") {
continue
}
file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
if err != nil {
t.Fatal(err)
}
imports := importedPaths(file)
for _, declaration := range file.Decls {
switch declaration := declaration.(type) {
case *ast.FuncDecl:
if exportedIdentifier(declaration.Name.Name) && usesInternalType(declaration.Type, imports) {
t.Errorf("%s exposes an internal type in its signature", declaration.Name)
}
case *ast.GenDecl:
for _, specification := range declaration.Specs {
typeSpec, ok := specification.(*ast.TypeSpec)
if !ok || !exportedIdentifier(typeSpec.Name.Name) {
continue
}
if typeSpec.Assign.IsValid() {
t.Errorf("%s is an exported type alias", typeSpec.Name)
}
structure, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
for _, field := range structure.Fields.List {
exposed := len(field.Names) == 0
for _, name := range field.Names {
exposed = exposed || exportedIdentifier(name.Name)
}
if exposed && usesInternalType(field.Type, imports) {
t.Errorf("%s exposes an internal type through a field", typeSpec.Name)
}
}
}
}
}
}
}
func importedPaths(file *ast.File) map[string]string {
paths := make(map[string]string, len(file.Imports))
for _, imported := range file.Imports {
path, err := strconv.Unquote(imported.Path.Value)
if err != nil {
continue
}
name := filepath.Base(path)
if imported.Name != nil {
name = imported.Name.Name
}
paths[name] = path
}
return paths
}
func usesInternalType(expression ast.Expr, imports map[string]string) bool {
internal := false
ast.Inspect(expression, func(node ast.Node) bool {
selector, ok := node.(*ast.SelectorExpr)
if !ok {
return true
}
packageName, ok := selector.X.(*ast.Ident)
if ok && strings.Contains(imports[packageName.Name], "/internal/") {
internal = true
}
return !internal
})
return internal
}
func exportedIdentifier(name string) bool {
for _, character := range name {
return unicode.IsUpper(character)
}
return false
}