Skip to content

feat: workspace topology correction — manifest in real git repo#13

Merged
TrekMax merged 19 commits into
mainfrom
claude/phase2.6-topology
Apr 10, 2026
Merged

feat: workspace topology correction — manifest in real git repo#13
TrekMax merged 19 commits into
mainfrom
claude/phase2.6-topology

Conversation

@TrekMax

@TrekMax TrekMax commented Apr 10, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 2.6: correct the workspace topology so the manifest lives in a real git repository, sibling of .east/, instead of as a loose file at workspace root.

This is a breaking change for existing workspaces — they must be re-initialized.

New east init modes

  • east init — create a template manifest repo (default dir: manifest)
  • east init -l <path> — use an existing local directory as manifest repo
  • east init -m <url> — clone a remote repository as manifest repo

Infrastructure

  • ManifestSelf — optional self: section in east.yml with path hint for canonical directory naming
  • ManifestConfig[manifest] section in .east/config.toml with path and file fields
  • Workspace rewrite — loads config first, derives manifest location from [manifest] section
  • New APIs: Workspace::manifest_repo_path(), Workspace::manifest_file_path()
  • Legacy fallback — workspaces without [manifest] config fall back to root/east.yml

Why this matters

Files that naturally live alongside east.yml (OpenOCD configs, toolchain files, build scripts, application source in T3 topology) are now accessible. This unblocks Phase 3's runner config path resolution.

Changes

Crate Change
east-manifest Add ManifestSelf struct, self: section parsing
east-config Add ManifestConfig, [manifest] section, path validation
east-workspace Rewrite Workspace::load(), add manifest_repo_path() / manifest_file_path()
east-cli Three-mode east init, update do_update() to use workspace manifest paths

Test plan

  • 5 manifest self: tests (parsing, reserved fields, coexistence)
  • 7 config [manifest] tests (validation, defaults, round-trip)
  • 4 workspace topology tests (paths, legacy fallback, nested discovery)
  • 8 init mode tests (local, template, error cases, end-to-end)
  • 10 update tests migrated to new topology
  • Existing config/ext-command tests pass via legacy fallback
  • Total: 165 tests, all passing locally
  • CI green on Linux, macOS, Windows

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays

claude added 14 commits April 10, 2026 08:43
Frozen design for Phase 2.6 — workspace topology correction:
- Manifest lives in a real git repo, sibling of .east/, not as loose file
- Three east init modes: -l (local), -m (remote clone), template (default)
- config.toml [manifest] section with path and file fields
- Manifest self: section for canonical directory naming hints
- Workspace API: manifest_repo_path(), manifest_file_path()
- New loading order: discover .east/ -> load config -> compute paths -> load manifest
- east update does NOT touch manifest repo (user manages via git)
- Clear error messages for Phase 1/2 workspace incompatibility
- Breaking change: existing workspaces must be re-initialized

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Red step: 5 tests covering manifest self: section:
- No self section → manifest_self is None
- self.path present → accessible
- self with no path → path is None
- Reserved fields (description, maintainers, repo-url) → parsed without error
- self coexists with projects and commands

All fail because manifest_self field does not exist on Manifest.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Add optional self: section to east.yml with:
- ManifestSelf struct with optional path field
- Serde rename to "self" (Rust keyword handled via rename)
- Reserved future fields (description, maintainers, repo-url)
  silently ignored by serde defaults
- Coexists with all other manifest sections

5 new tests. All 61 manifest tests pass.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
…tion

Red step: 7 tests covering ManifestConfig:
- Parse from store with path and file fields
- file defaults to east.yml when absent
- Missing path → error with Phase 1/2 upgrade hint
- Rejects absolute path, dotdot (..), empty path
- Round-trip: write to store then read back

All fail because manifest_config module does not exist.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Add ManifestConfig struct for the [manifest] section in config.toml:
- from_store() reads manifest.path (required) and manifest.file
  (defaults to east.yml)
- Validation: path must be relative, non-empty, no .. components
- write_to_store() for round-trip persistence
- ManifestSectionMissing error with Phase 1/2 upgrade hint message
- InvalidManifestPath error for bad path values

7 new tests. All pass.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
…t config

Red step: 4 tests for Phase 2.6 workspace topology:
- manifest_repo_path() returns <root>/<manifest.path>
- manifest_file_path() returns <root>/<manifest.path>/<manifest.file>
- Missing [manifest] section in config detected
- Discovery from inside manifest-repo/src/deep/ finds .east/ correctly

All fail because manifest_repo_path() and manifest_file_path() don't exist.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Add manifest_repo_path() and manifest_file_path() to Workspace:
- discover() now loads .east/config.toml and extracts [manifest] section
- manifest_repo_path = <root>/<config.manifest.path>
- manifest_file_path = <root>/<config.manifest.path>/<config.manifest.file>
- Falls back to root when config lacks [manifest] (legacy compatibility)
- Legacy manifest_path() preserved for gradual migration
- Discovery from inside manifest-repo correctly finds .east/ at parent

4 new tests. All 11 workspace tests pass.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Red step: 7 tests for Phase 2.6 init modes:

Mode L (local):
- init -l creates .east/ with [manifest] config pointing to local dir
- init -l fails if .east/ already exists
- init -l fails if manifest file missing in dir
- init -l fails if dir doesn't exist

Mode T (template):
- Default dir "manifest" with east.yml, .git, .gitignore
- Custom dir name
- No initial commit (unstaged template)

5 of 7 fail because -l flag and template mode don't exist yet.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Rewrite east init as a three-mode dispatcher:

Mode L (-l <path>): use existing local dir as manifest repo
- Creates .east/ in parent directory
- Writes [manifest] section to config.toml
- Warns if not a git repo

Mode M (-m <url>): clone remote repo as manifest repo
- Clones to <workspace>/<repo-name>/
- Supports --mr for revision, --mf for manifest filename
- Verifies manifest file exists after clone

Mode T (default): create template manifest repo
- Creates template east.yml with commented examples
- Runs git init, creates .gitignore
- No initial commit (user inspects first)

All modes: --force to reinitialize, .east/ exists = error otherwise.

7 new integration tests. All pass.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
…t paths

- do_update() now discovers workspace and uses manifest_path() instead
  of hardcoding workspace_root.join("east.yml")
- All commands (list, status, manifest --resolve, ext commands) already
  use ws.manifest_path() which delegates to the new topology
- New integration test: init -l then update end-to-end verifies the full
  Phase 2.6 topology works (manifest in nested repo, project cloned)

8 init_v2 tests pass (7 + 1 new end-to-end).

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
…logy

Mechanical migration of integration tests to new nested manifest-repo layout:

Migrated files:
- crates/east-cli/tests/cli_init.rs: replaced with Phase 2.6 tests
  (was cli_init_v2.rs, old cli_init.rs removed)
- crates/east-cli/tests/cli_update.rs: setup_multi_project_workspace()
  now creates manifest-repo inside workspace and uses `east init -l`
  instead of the old positional-argument init

Unchanged files (work via legacy fallback):
- crates/east-cli/tests/cli_config.rs
- crates/east-cli/tests/cli_ext_commands.rs
- crates/east-cli/tests/cli_script_path.rs

All 165 tests pass.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Retrospective covering topology correction: three init modes,
ManifestSelf, ManifestConfig, Workspace rewrite, test migration.
165 tests, all passing.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Update status to Phase 2.6 complete. Quick Start now shows:
- east init (template mode)
- east init -l ./my-app (local mode)
- east init -m <url> (remote mode)

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
Copilot AI review requested due to automatic review settings April 10, 2026 11:33
Rustdoc treats <path>, <url>, <dir> as HTML tags and [manifest] as
an intra-doc link. Use backtick code spans and \[ \] escapes instead.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes Phase 2.6 by correcting the workspace topology so the manifest lives inside a real git repository within the workspace (sibling to .east/), rather than as a loose east.yml at the workspace root. It introduces new config/manifest metadata to locate the manifest repo + file, and updates the CLI to support the new east init modes.

Changes:

  • Add [manifest] workspace config (path, file) and use it to derive manifest repo/file paths (with legacy fallback).
  • Add optional manifest self: section (path hint) and parsing/tests.
  • Rewrite east init into three modes (-l, -m, template default) and migrate integration tests/docs to the new topology.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
README.zh-CN.md Updates status and Quick Start examples for Phase 2.6 init modes/topology.
README.md Updates status and Quick Start examples for Phase 2.6 init modes/topology.
docs/dev/phase-2.6.zh-CN.md Adds Phase 2.6 dev notes (CN) summarizing delivery and tests.
docs/dev/phase-2.6.md Adds Phase 2.6 dev notes (EN) summarizing delivery and tests.
docs/design/phase-2.6.zh-CN.md Adds Phase 2.6 design doc (CN) describing topology and error model.
docs/design/phase-2.6.md Adds Phase 2.6 design doc (EN) describing topology and error model.
crates/east-workspace/src/workspace.rs Loads manifest paths from .east/config.toml; adds manifest repo/file APIs + legacy fallback.
crates/east-workspace/src/lib.rs Adds tests for manifest-path resolution and nested discovery in the new topology.
crates/east-workspace/Cargo.toml Adds dependency on east-config for reading [manifest] section.
crates/east-manifest/tests/manifest_self.rs New tests covering optional self: parsing behavior.
crates/east-manifest/src/resolve.rs Ensures resolved manifest initializes manifest_self.
crates/east-manifest/src/model.rs Adds ManifestSelf model and optional self: field on Manifest.
crates/east-manifest/src/lib.rs Re-exports ManifestSelf and updates manifest tests to include the new field.
crates/east-config/tests/manifest_config.rs New tests for [manifest] config parsing/validation/round-trip.
crates/east-config/src/manifest_config.rs Implements ManifestConfig and validation for manifest.path (and default for file).
crates/east-config/src/lib.rs Exposes manifest_config module and re-exports ManifestConfig.
crates/east-config/src/error.rs Adds ManifestSectionMissing and InvalidManifestPath errors.
crates/east-cli/tests/cli_update.rs Migrates update integration tests to new topology (manifest repo inside workspace).
crates/east-cli/tests/cli_init.rs Reworks init tests around Phase 2.6 local/template modes and init+update E2E.
crates/east-cli/src/main.rs Implements three-mode east init, writes [manifest] config, updates update path resolution.
Cargo.lock Records new east-config dependency for east-workspace.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/east-workspace/src/workspace.rs Outdated
Comment on lines +99 to +110
/// Path to the manifest file.
///
/// Computed from `config.manifest.path` + `config.manifest.file`.
/// Falls back to `<root>/east.yml` for legacy compatibility.
#[must_use]
pub fn manifest_file_path(&self) -> &Path {
self.manifest_file_path.as_deref().unwrap_or_else(|| {
// This is a static fallback — we can't return a reference to a
// temporary, so use the legacy path which is derived from root.
// In practice this path is only used when config hasn't been loaded.
&self.root
})

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manifest_file_path()'s fallback returns &self.root (a directory), not the legacy manifest file path (<root>/east.yml) as documented. Any caller using the new API will try to read a directory as a file. Consider ensuring manifest_file_path is always populated (e.g., set it to <root>/east.yml when config/[manifest] is missing) or change the API to return an owned PathBuf so the legacy join can be returned safely.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +51
let file = store
.get("manifest.file")
.and_then(ConfigValue::as_str)
.unwrap_or("east.yml")
.to_string();

Ok(Self { path, file })

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ManifestConfig::from_store() validates manifest.path but does not validate manifest.file. As a result, values like ../outside.yml or absolute paths could escape the manifest repo directory when Workspace builds repo.join(file), allowing the workspace to read an arbitrary file as the manifest. Add validation for manifest.file similar to manifest.path (reject empty/absolute/.. components; optionally restrict to a plain filename and/or reject backslashes to keep TOML portable across platforms).

Copilot uses AI. Check for mistakes.
Comment thread crates/east-cli/src/main.rs Outdated
Comment on lines +286 to +295
// Derive repo name from URL
let repo_name = url
.rsplit('/')
.next()
.unwrap_or("manifest")
.strip_suffix(".git")
.unwrap_or_else(|| url.rsplit('/').next().unwrap_or("manifest"));

let clone_dest = workspace_root.join(repo_name);

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repo directory name derivation from url can produce an empty string for URLs that end with / (e.g. https://host/org/repo/), which would make clone_dest equal the workspace root and potentially clobber/overlap .east/. Consider trimming trailing slashes and validating repo_name is non-empty and filesystem-safe; also handle SCP-style git URLs (git@host:org/repo.git) which won't parse correctly with rsplit('/').

Copilot uses AI. Check for mistakes.
Comment on lines +333 to +336
bail!(
"directory '{}' already exists. Use a different name or --force.",
dir_name
);

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In template mode, when the target directory already exists, the error message suggests using --force, but --force does not bypass this check (the bail is unconditional). Either honor --force by removing/reusing the existing directory, or update the message to avoid advertising a non-functional flag for this case.

Suggested change
bail!(
"directory '{}' already exists. Use a different name or --force.",
dir_name
);
bail!("directory '{}' already exists. Use a different name.", dir_name);

Copilot uses AI. Check for mistakes.
/// Force init even if .east/ already exists.
#[arg(long)]
force: bool,
/// Directory name for template mode or workspace dir for -m mode.

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dir is accepted as a positional argument for all init modes, but in -l (local) mode it is ignored (the code only uses local_path). This can lead to confusing CLI behavior where extra args are silently accepted but not used. Consider adding clap constraints so dir conflicts with --local, or split init modes into distinct subcommands to avoid ambiguous parsing.

Suggested change
/// Directory name for template mode or workspace dir for -m mode.
/// Directory name for template mode or workspace dir for -m mode.
#[arg(conflicts_with = "local")]

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +273
/// Mode M: clone a remote URL as manifest repo.
async fn cmd_init_remote(
url: &str,
revision: Option<&str>,
manifest_file: &str,
workspace_dir: Option<&str>,
force: bool,
) -> miette::Result<()> {
let cwd = std::env::current_dir().into_diagnostic()?;

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New remote init mode (east init -m) introduces URL parsing, clone destination naming, and optional revision checkout, but there are no integration tests exercising -m in the current test suite (no init -m invocations). Adding at least one end-to-end test for -m (including --mr) would help prevent regressions and cover edge cases like directory naming and manifest file validation.

Copilot uses AI. Check for mistakes.
claude added 4 commits April 10, 2026 11:41
Windows git clone rejects \\?\ UNC paths produced by
std::fs::canonicalize. Apply strip_unc_prefix() to all project
paths derived from workspace_root.join() in do_update(),
cmd_list(), and cmd_status().

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
… validation

On Windows, Path::new("/abs/path").is_absolute() returns false because
Windows requires a drive letter for absolute paths. Add explicit check
for leading / or \ to catch Unix-style absolute paths cross-platform.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
1. manifest_file_path() fallback: was returning &self.root (directory)
   instead of <root>/east.yml. Now always populated during discover().

2. manifest.file validation: add validate_manifest_file() rejecting
   path separators, .., empty, and absolute paths to prevent directory
   escape via repo.join(file).

3. URL repo name derivation: handle trailing slashes, SCP-style URLs
   (git@host:org/repo.git), empty basename → fallback to "manifest".

4. Template mode --force: now honors --force for existing directory
   (was unconditionally bailing).

5. dir arg conflicts_with local: prevent silent acceptance of positional
   dir argument in -l mode.

6. Add integration test for east init -m (Mode M) covering clone from
   file:// URL, directory naming, and config writing.

166 tests, all passing.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
The file:// URL format differs between Unix and Windows (file:///C:/
vs file://C:\), causing git clone to misinterpret the destination.
Use a local filesystem path directly, which git clone handles
consistently on all platforms.

https://claude.ai/code/session_01XPaw9o6u8dbEfgjyyVNays
@TrekMax TrekMax merged commit 1e6e352 into main Apr 10, 2026
14 of 16 checks passed
@TrekMax TrekMax deleted the claude/phase2.6-topology branch April 11, 2026 06:30
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.

3 participants