Skip to content
2 changes: 1 addition & 1 deletion .spec/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ EasyP v1 separates CLI composition, module operations, generation preparation, a
|-----------|--------------------------------|--------------|
| <code>internal/api</code> | Flags, process paths/environment, adapter construction, policy command orchestration, output and exit status | Module/generation operations, configuration, rules, core, concrete adapters |
| <code>internal/modules</code> | Dependency selection, lock validation, source roots and ownership, manifest edits, coordinated project-file updates | V1 models, <code>Source</code>/<code>Cache</code> contracts, metadata reader, filesystem |
| <code>internal/adapters/gitmodules</code> | Git candidates/revisions, checkout lifetime, persistent object cache and locking, tracked-file hashes and installation | System Git, <code>module_config</code>, module contracts, filesystem |
| <code>internal/adapters/gitmodules</code> | Git candidates/revisions, checkout lifetime, persistent object cache and locking, immutable materialized snapshot hashes and installation | System Git, <code>module_config</code>, module contracts, filesystem |
| <code>internal/adapters/module_config</code> | Adapt repository metadata into a named module and import roots | Native <code>protobuf.mod</code> parser, legacy EasyP and Buf readers |
| <code>internal/migration</code> | Preview plans, legacy conversion, integrity verification gates, backups and rollback | V1 models, module resolution, explicit migration repository, filesystem |
| <code>internal/workspace</code> | Repository boundary and ancestor/module/config discovery | Filesystem |
Expand Down
30 changes: 30 additions & 0 deletions .spec/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,36 @@ easyp migrate --dir . --module github.com/acme/contracts --resolve-lock --write

Flag-only invocation previews without file writes unless <code>--write</code> is supplied. If dependency integrity checks are required, application remains blocked until <code>--resolve-lock</code> authorizes them. Existing native outputs are not overwritten with conflicting candidates; legacy replaced files receive <code>.v0.bak</code> backups and <code>easyp.lock</code> is retained unchanged. Legacy <code>version</code> metadata such as <code>v1alpha</code> is accepted and omitted with a warning; <code>deps: null</code> means an empty dependency list. Keys that the v0 parser ignored are also omitted with source-path warnings instead of being assigned invented v1 semantics, while the byte-identical v0 backup preserves them. Known fields with invalid or ambiguous values still block migration. The plan rechecks observed files before applying and rolls back ordinary write failures, but multiple replacements are not process-crash atomic. See [internal/migration/migration.go](../internal/migration/migration.go), [apply.go](../internal/migration/apply.go), and [migration review context](config/review-migration-and-polish.md).

Local directory selection may become literal <code>generate.paths</code> selectors
without moving sources. The plan compares whole roots, then the original
module-directory-relative paths, then complete protobuf packages for cases
that cannot be represented by literal paths.
Each candidate must preserve the exact import-name-to-physical-source map.
Omitted or empty legacy roots keep <code>.</code>; native manifests with no
<code>roots</code> use that same default. Same-package files outside selected
paths do not become generation targets, including ignored Gradle build copies.
Changed import names, hidden/vendor/nested-module boundary changes and inferred
local filters mixed with whole-module Git inputs stay blocked. The plan rechecks
sources, paths and packages before applying. See [source selection](config/package-selection.md).

Historical <code>easyp.lock</code> entries may use a full SemVer tag followed by
Git's peeled-ref suffix <code>^{}</code>. Migration verifies the corresponding
tag and legacy content hash before writing the native lock; it never rewrites
the historical lock or switches to HEAD. Peeled branches, abbreviated refs,
repeated suffixes and pseudo-version-shaped peeled tags are rejected.
Released v0 lock hashes cover the installed <code>git archive '*.proto'</code>
contents after legacy root rewrites, while the new lock covers the materialized v1
snapshot. Migration verifies either the historical archive hash or the existing
whole-tree hash at the pinned revision. It rejects archive attributes that omit
or alter proto sources rather than silently changing their contracts.
Internal file, directory, import-root and metadata symlinks are supported.
Logical paths keep their protobuf import names. Git targets resolve only from
the pinned tree; installed snapshots contain regular resolved bytes and work
with <code>core.symlinks=false</code>. Selected inputs cannot escape their owner,
cross undeclared nested repositories/submodules or form cycles. Invalid unused
auxiliary links are omitted. Migration verifies historical archive contents
separately; it never substitutes the new snapshot hash for a v0 input hash.

### <code>easyp ls-files [flags]</code>

Lists sources from the working directory's native manifest, or a default local <code>.</code> root if that directory has no manifest. Unlike <code>mod</code>/<code>get</code>, this handler does not search upward for a module. The global config flag does not choose its root.
Expand Down
4 changes: 3 additions & 1 deletion .spec/PACKAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ Section-scoped <code>extends</code> is implemented in <code>internal/policy</cod

| Package | Main files / contract |
|---------|-----------------------|
| <code>internal/adapters/gitmodules</code> | <code>cache.go</code>, <code>git.go</code>: cache layout and Git execution; <code>object_cache.go</code>, <code>object_lock_unix.go</code>, <code>object_lock_windows.go</code>: reusable Git object repositories and OS locks; <code>checkout.go</code>, <code>git_source.go</code>: revision/candidate selection; <code>download.go</code>, <code>files.go</code>: installation and tracked-file hashing; <code>identity.go</code>: optional Git origin identity; <code>migration.go</code>, <code>migration_config.go</code>, <code>migration_selection.go</code>: historical revision/hash verification |
| <code>internal/sourceview</code> | Bounded logical resolve/open/walk over standard io/fs; local os.Root reads and alias topology checks |
| <code>internal/adapters/gitsnapshot</code> | Immutable Git tree/blob filesystem, SHA-1/SHA-256 repository support and host path collision checks |
| <code>internal/adapters/gitmodules</code> | <code>cache.go</code>, <code>git.go</code>: cache layout and Git execution; <code>object_cache.go</code>, <code>object_lock_unix.go</code>, <code>object_lock_windows.go</code>: reusable Git object repositories and OS locks; <code>checkout.go</code>, <code>git_source.go</code>: revision/candidate selection; <code>download.go</code>, <code>files.go</code>: installation and materialized snapshot hashing; <code>identity.go</code>: optional Git origin identity; <code>migration.go</code>, <code>migration_config.go</code>, <code>migration_selection.go</code>: historical revision/hash verification |
| <code>internal/adapters/plugin</code> | Local, remote, built-in WASM and command executors; <code>Info</code> carries the explicit local execution directory |
| <code>internal/adapters/go_git</code> | Historical project-tree walkers for breaking checks |
| <code>internal/adapters/console</code> | Platform command execution |
Expand Down
4 changes: 2 additions & 2 deletions .spec/config/dependency.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ See [Go major version suffixes](https://go.dev/ref/mod#major-version-suffixes) a

## Lock and cache

<code>protobuf.lock</code> is YAML with integer <code>version: 1</code> and a <code>modules</code> sequence. Each entry has <code>source</code>, <code>version</code>, <code>commit</code> and <code>hash</code>; the resolver sorts by source. Commits must be full 40- or 64-character hexadecimal hashes; <code>hash</code> is <code>h1:</code> plus a base64 SHA-256 digest. A commit-valued version must equal the commit. Duplicate sources, unknown fields and multiple YAML documents are rejected. The hash covers the installed regular-file snapshot using Go's directory-hash algorithm. Git symlinks and submodules are omitted, even when a symlink is materialized as a regular file by <code>core.symlinks=false</code>. Dependency config files must be regular Git files. Buf file filters are applied before hashing and installation; non-proto regular files remain included. Legacy lock migration retains its stricter non-regular-file rejection because it must reproduce the old hash.
<code>protobuf.lock</code> is YAML with integer <code>version: 1</code> and a <code>modules</code> sequence. Each entry has <code>source</code>, <code>version</code>, <code>commit</code> and <code>hash</code>; the resolver sorts by source. Commits must be full 40- or 64-character hexadecimal hashes; <code>hash</code> is <code>h1:</code> plus a base64 SHA-256 digest. A commit-valued version must equal the commit. Duplicate sources, unknown fields and multiple YAML documents are rejected. The hash covers the installed regular-file snapshot using Go's directory-hash algorithm. Internal file, directory/root and metadata aliases resolve from the pinned Git tree and are materialized under logical names; Git pointer targets must be relative and stay within that tree. Selected unsafe, dangling, cyclic or submodule-crossing inputs fail. Raw descendant submodules remain opaque skipped boundaries. Invalid unused auxiliary links are omitted. Buf filters select logical proto paths before hashing and installation; regular non-proto files and safe non-proto aliases remain included. The sole v1 hash policy is h1/Hash1 over this snapshot. Cache identity includes source, commit and hash. Historical v0 archive reconstruction is used only by explicit migrate input verification.

The CLI resolves <code>EASYPPATH</code> once for a command that needs the cache (default <code>$HOME/.easyp</code>). <code>gitmodules.Cache</code> owns <code>&lt;EASYPPATH&gt;/v1/git</code>, source keys, temporary checkout names and installation paths. Installed snapshots live under its <code>modules/&lt;source-key&gt;/&lt;commit&gt;</code> layout; reusable bare object stores live under <code>objects/&lt;remote-key&gt;</code>, with OS locks for concurrent fetches. Application callers request cached module metadata and its physical directory; they do not assemble cache paths.
The CLI resolves <code>EASYPPATH</code> once for a command that needs the cache (default <code>$HOME/.easyp</code>). <code>gitmodules.Cache</code> owns <code>&lt;EASYPPATH&gt;/v1/git</code>, source keys, temporary checkout names and installation paths. Installed snapshots live under its <code>modules/&lt;source-key&gt;/&lt;snapshot-key&gt;</code> layout, where the snapshot key hashes commit plus content hash; reusable bare object stores live under <code>objects/&lt;remote-key&gt;</code>, with OS locks for concurrent fetches. Application callers request cached module metadata and its physical directory; they do not assemble cache paths.

<code>Cache.Install</code> verifies locked contents, including cache hits. A newly installed snapshot is checked with the full <code>h1:</code> directory hash. On Darwin/Linux, later unchanged hits use a sidecar verification stamp keyed by the expected lock hash plus an inode/ctime metadata fingerprint; any metadata change invalidates the stamp and forces the full directory hash again. Content changes therefore remain detectable even when size and mtime are restored. Platforms without a strong change token keep the full-hash path. The stamp is an optimization of the per-user local cache, not an additional source of dependency identity. Cached objects support shallow pinned-commit fetches with an advertised-history fallback; neither path substitutes HEAD for an unavailable pin. <code>Cache.Cached</code> reads installed metadata without downloading or rewriting module contents and requires prior verification. Git credentials and transport configuration remain the responsibility of system Git.

Expand Down
60 changes: 55 additions & 5 deletions .spec/config/package-selection.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Exact protobuf package selection
# Protobuf package and path selection

A nonempty generate.packages selects exact protobuf package names within the
modules selected by a generation project. Empty or omitted selects all source
files, preserving the existing behavior. Names follow protobuf identifier
modules selected by a generation project. Empty or omitted means all packages
within generate.paths; without either filter, all module sources participate. Names follow protobuf identifier
syntax; they are not prefixes, filesystem paths, globs or expressions.

~~~yaml
Expand All @@ -16,8 +16,9 @@ plugins:
with_imports: true
~~~

All files declaring a selected package participate. Matching is across the
project's selected modules, not separately required in every module. Modules
All files declaring a selected package within the selected paths participate.
Matching is across the project's selected modules, not separately required in
every module. Modules
with no matching package do not execute plugins or produce empty descriptor
sets. Unknown names fail before any plugin, including partially matched lists.
Duplicate names are idempotent. Explicit package validation applies even to an
Expand All @@ -41,6 +42,55 @@ before execution remain unchanged. Selection never modifies dependency state,
proto packages, import names or a published lock. Frozen checks still verify
the selected module graph; local replacements remain forbidden in frozen mode.

## Literal paths

<code>generate.paths</code> selects exact module-relative files or directory
subtrees across the project's selected modules. Empty means all module sources;
<code>.</code> explicitly selects all. Matching is component-bounded: <code>mcp</code>
does not select <code>mcp-copy</code>. Canonical relative paths are required;
absolute paths, traversal, backslashes and globs are rejected. These selectors
are separate from plugin <code>opts.paths</code>, which controls output layout.
The base is each selected module's directory, not its import roots or the
generator file. For roots <code>api</code>, <code>api/easyp</code> selects files
whose compiler import names start with <code>easyp/</code>.

Module entries may be strings or objects with <code>module</code>, optional
<code>paths</code> and optional <code>packages</code>. Module filters intersect the
global filters. Global selectors must match across selected modules; scoped
selectors must match their own module's final sources. Identical repeated
selections are idempotent (selector order and repetition do not matter);
conflicting selectors for one resolved module in a project fail before plugins.
Global filters remain available without listing a module.

~~~yaml
version: v1
generate:
paths: [mcp]
plugins:
- name: go
out: .
opts: {paths: source_relative}
~~~

Paths and packages intersect. Each selector must match a resulting source in
at least one selected module. Unknown or partially unmatched lists fail before
plugins or descriptor writes, including no-plugin projects and parents selected
with <code>--all</code>. Source discovery respects existing module/Buf filters;
required imports outside the selected paths still compile. No automatic Git
ignore policy is introduced. Roots, source locations and import names stay fixed.

The migration wizard prefers exact directory paths after whole-root equality.
It falls back to complete package selectors only when paths cannot preserve a
mixed-root selection. The current import-name/physical-file maps must match,
and sources plus fixed paths/packages are rechecked before apply. Empty inferred
selection, alias/boundary changes and local filters combined with whole-module
Git generation inputs remain rejected. Future same-package build copies outside
a selected directory do not widen that directory's targets. Package fallback
previews still warn that future files declaring selected packages participate.
The sole local module is inferred from the generator's location rather than
repeated in <code>generate.modules</code>. Migration does not add an unconfigured
<code>options.go.package_prefix</code>; omitted values retain normal inheritance.

Regressions verify exact/prefix distinction, multiple files, multiple modules,
no-plugin failures, invalid unselected/required sources, custom options, per-
plugin imports, both descriptor modes and compiled Go output with a local
Expand Down
4 changes: 2 additions & 2 deletions .spec/config/review-context-and-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ easyp generate --all
The all and project flags are mutually exclusive. Recursive discovery skips
hidden directories, easyp_vendor, node_modules and nested Git repositories.
A project in a skipped directory can still be deliberately selected with
project. Automatic selection refuses a symlink configuration file; explicit
project selection is required to use it. No name-based directory denylist is
project. Internal configuration aliases are resolved within the workspace
boundary for both automatic and explicit selection. No name-based directory denylist is
claimed to establish a trust boundary: choosing all explicitly authorizes
recursive generation, including command plugins in the selected tree.

Expand Down
18 changes: 17 additions & 1 deletion .spec/config/review-migration-and-polish.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,27 @@ physical files and protobuf import names must remain equivalent.

Legacy direct/indirect directives become require. Old full-SHA/pseudo-version
pins retain their commit. A tag-only pin is accepted only after verifying the
legacy installed-tree hash. The new full tracked-repository hash is calculated
legacy installed-tree hash. The new materialized snapshot hash is calculated
independently. Missing historical pins, retags and hash mismatches fail. The old
easyp.lock remains byte-identical. Empty projects receive a native empty lock,
so the retained old lock cannot block later normal module commands.

Internal file, directory, import-root and metadata symlinks are supported.
Logical paths keep their protobuf import names. Git targets resolve only from
the pinned tree; installed snapshots contain regular resolved bytes and work
with <code>core.symlinks=false</code>. Selected inputs cannot escape their owner,
cross undeclared nested repositories/submodules or form cycles. Invalid unused
auxiliary links are omitted. Migration verifies historical archive contents
separately; it never substitutes the new snapshot hash for a v0 input hash.

Git index framing, stage and local-path validation live in the pure
internal/adapters/gitindex parser. Snapshot selection and metadata preflight
apply their own mode policies. Migration reads one index, records the selected
regular files and omitted auxiliary links, then verifies historical contents
in migration_integrity.go. FetchMigration keeps source equivalence and native
hash calculation as separate steps; no-link whole-tree proof and archive-only
proof after omitted links remain distinct.

The application transaction stages all results and .v0.bak recovery copies,
checks observed contents/permissions/source scope again, and rolls back ordinary
failures. Conflicting existing files and unsafe symlink destinations are refused.
Expand Down
Loading
Loading