diff --git a/.codebase-memory/artifact.json b/.codebase-memory/artifact.json index 122c729..b72abad 100644 --- a/.codebase-memory/artifact.json +++ b/.codebase-memory/artifact.json @@ -1,11 +1,11 @@ { "schema_version": 2, - "commit": "550f92d7c6f197d7f4996919a418d9ca0174eb07", - "indexed_at": "2026-09-12T16:21:16Z", + "commit": "7f46c947dd56f437077df475ba5bba8fd864e2e1", + "indexed_at": "2026-09-12T16:26:42Z", "project": "home-eric-Workspace-ConfigKit", "nodes": 1625, - "edges": 7835, + "edges": 7824, "original_size": 8126464, - "compressed_size": 1330563, + "compressed_size": 1329933, "compression_level": 3 } \ No newline at end of file diff --git a/.codebase-memory/graph.db.zst b/.codebase-memory/graph.db.zst index ca7aab6..a70f3c4 100644 Binary files a/.codebase-memory/graph.db.zst and b/.codebase-memory/graph.db.zst differ diff --git a/docs/adr/0006-public-facade.md b/docs/adr/0006-public-facade.md index 061ca34..db32c36 100644 --- a/docs/adr/0006-public-facade.md +++ b/docs/adr/0006-public-facade.md @@ -2,7 +2,7 @@ ## Status -Accepted. This records a design decision, not implementation completion. +Accepted and implemented on 2026-09-12. ## Date @@ -10,32 +10,31 @@ Accepted. This records a design decision, not implementation completion. ## Context -The root `configkit` package currently combines the supported consumer API with -the loading, decoding, inspection, and rendering machinery. That makes ordinary -consumer navigation harder and risks exposing implementation details through +The root `configkit` package formerly combined the supported consumer API with +loading, decoding, inspection, and rendering machinery. That made ordinary +consumer navigation harder and risked exposing implementation details through future public declarations. Moving files alone would not preserve the identity, -method sets, callbacks, ownership rules, or reflection-sensitive behavior of -the root's public types. +method sets, callbacks, ownership rules, or reflection-sensitive behavior of the +root's public types. ## Decision 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 -in a cohesive `internal/engine`; the root facade may hold private engine state -and forward deliberately selected methods. The dependency direction is root to -engine and its supporting internal packages. Internal production packages do -not import the root package. +named types defined there. Lifecycle machinery lives in a cohesive +`internal/engine`; the root facade holds private engine state and forwards +deliberately selected methods. The dependency direction is root to engine and +its supporting internal packages. Internal production packages do not import the +root package. The facade will use root-owned wrappers and explicit private conversions. It will not use public aliases to engine types, anonymous embedding that promotes engine methods, public engine types in signatures or fields, or public bridge methods. Existing document and YAML boundaries remain intact. -The migration may use a temporary internal candidate facade while the unchanged -root remains the only production entry point. That duplication is a bounded +The migration used a temporary internal candidate facade while the unchanged root +remained the only production entry point. That duplication was a bounded relocation mechanism, not a second supported package, feature path, or runtime -selection mechanism. The final cutover removes the temporary candidate and old -root implementation together. +selection mechanism; the final cutover removed it and the old root implementation. The [architecture policy](../policy/architecture.md#public-api-and-dependency-direction) owns the binding boundary. The [public-facade roadmap](../roadmap/public.md) diff --git a/docs/development.md b/docs/development.md index 22d45d9..9215fc6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -98,15 +98,15 @@ code changes. Use the component overview to find the corresponding implementatio | 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'` | -| 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'` | -| 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'` | -| 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'` | -| 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'` | -| 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'` | -| 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'` | +| 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 ./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 ./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 ./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 ./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 ./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 ./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'` | -| 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'` | ## 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: ```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 . -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 ``` diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 897aedc..e2e3776 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -1,94 +1,39 @@ # Internal Component Overview -ConfigKit exposes the root `configkit` package and keeps its document and YAML -representations private. The implemented components and their owners are: +ConfigKit exposes one supported root `configkit` package. The root owns public +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 - 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. +Implemented component owners: -Dependency direction remains inward: the public lifecycle coordinates private -source, document, schema, and inspection helpers. Semantic paths stay in the root -package; `internal/document` owns only representation and cloning and never imports -the root package. Only `internal/yaml` imports the upstream YAML package. Maintained -consumer examples use the public package and standard library. +- `internal/engine` owns source acquisition, composition, schema inspection, + decoding, lifecycle coordination, callback auditing, provenance/history, + secret resolution and bootstrap, and safe inspection rendering. It never + imports the root package. +- 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, -loading lifecycle, path imports, composition, profile/CLI overlay, version checking, -recorded resolution, typed secret references/bootstrap, semantic validation, -metadata, secret classification, redacted effective rendering, historical -explanation, and ownership behavior. -The [consumer adoption guide](../consumers/README.md) owns current application-fit -and migration guidance. +Dependency direction is root to engine to document/YAML support. The root may +import only `internal/engine` among project internal production packages; engine +and its supporting packages do not import root. The root boundary test enforces +these constraints and rejects exported aliases, fields, or signatures involving +internal types. -The source-extraction inventory records the application foundations used by the -implementation: - -- [Extraction inventory](extraction.md) - -The [architecture policy](../policy/architecture.md) owns binding boundaries and -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. +The [configuration contract](../config.md) owns implemented YAML and lifecycle +semantics. The [consumer adoption guide](../consumers/README.md) owns application +guidance. The [architecture policy](../policy/architecture.md) owns contributor +boundaries, and [ADR 0006](../adr/0006-public-facade.md) records the facade +decision. See the [development guide](../development.md) for validation commands +and [release procedure](../release.md) for source-candidate gates. diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index c881b03..32f1bf4 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -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, but internal production packages must not import the root. Explicit forwarding and local conversion are acceptable where they preserve type identity, -ownership, callback lifetime, and inspection behavior. The accepted -root-wrapper boundary and its migration rationale are recorded in -[ADR 0006](../adr/0006-public-facade.md); its implementation remains tracked by -the public-facade roadmap. +ownership, callback lifetime, and inspection behavior. The accepted root-wrapper +boundary and its migration rationale are recorded in +[ADR 0006](../adr/0006-public-facade.md); completed implementation evidence is +recorded in the public-facade roadmap. Separate input access, document processing, composition, decoding, metadata, and rendering responsibilities without prescribing an interface or package for each. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 67a98c4..01e9b71 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,8 +1,8 @@ # Public facade: implementation plan -Status: not started. Baseline: `28e7fc5f690b36c60e4d03586afcff00fb1dee90`. -The [feature roadmap](public.md) owns the target architecture and user intent. -This plan owns implementation decisions, ordered work, validation, and handoffs. +Status: complete (2026-09-12). Baseline: `28e7fc5f690b36c60e4d03586afcff00fb1dee90`. +The [feature roadmap](public.md) owns the implemented architecture and user intent. +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; 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 -Status: not started. +Status: complete (2026-09-12). 1. Compare the final complete exported API against the baseline. Audit public 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 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 None requiring a user decision were identified. Wrappers, public type ownership, diff --git a/docs/roadmap/public.md b/docs/roadmap/public.md index b1312b0..56d16bc 100644 --- a/docs/roadmap/public.md +++ b/docs/roadmap/public.md @@ -1,8 +1,8 @@ # Public facade and internal implementation -Status: agreed direction; not implemented. This roadmap defines the desired end -state and user intent. The [implementation plan](implementation.md) owns the -ordered work, concrete adapter design, validation commands, and handoffs. +Status: implemented on 2026-09-12. This roadmap records the intended architecture +and user intent; the completed [implementation plan](implementation.md) records +the ordered work, validation, and handoffs. ## Intent @@ -31,13 +31,13 @@ design. Existing [configuration contracts](../config.md) and exported Go declaration comments remain authoritative. The user-approved wrapper boundary is 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: -source acquisition, imports, composition and overlays, reflection descriptors, -decoding, resolution and validation coordination, provenance, history, secret -handling, and rendering. The [component overview](../internal/overview.md) owns -the detailed inventory. +The root owns public declarations, records, wrappers, and deliberate forwarding. +`internal/engine` owns source acquisition, imports, composition and overlays, +reflection descriptors, decoding, resolution and validation coordination, +provenance, history, secret handling, and rendering. The +[component overview](../internal/overview.md) owns the detailed inventory. Two established internal boundaries already exist: `internal/document` owns the 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 type ownership, not just file relocation. -## Target architecture +## Architecture ### 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 [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 internally owned, consumers require no migration, and representative diff --git a/import_boundary_test.go b/import_boundary_test.go index 11b7c3c..9a8a2df 100644 --- a/import_boundary_test.go +++ b/import_boundary_test.go @@ -1,6 +1,7 @@ package configkit import ( + "go/ast" "go/parser" "go/token" "io/fs" @@ -8,6 +9,7 @@ import ( "strconv" "strings" "testing" + "unicode" ) 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") { 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 }) @@ -48,3 +59,92 @@ func TestDependencyImportBoundary(t *testing.T) { 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 +}