From 867afb8033614573ecebee7aa46e46baedb26191 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 18:13:58 +0000 Subject: [PATCH] feat(repo_id): prototype FDR-0019 scoped repo resolution (dodder-side) Implement the user-visible slice of FDR-0019 entirely within dodder, ahead of the madder env_dir.RepoId grammar change the FDR ultimately routes through. Several named repos can now coexist per scope under a repos// layout and are addressed by the name portion of -repo_id (and DODDER_REPO_ID). - internal/bravo/repo_id: new location selector type parsing the FDR grammar (name / .name / ..name / //name / /name / ~name, plus the legacy nameless . / / / empty) with canonical single-dot String(). - env_dir.NestUnderRepoName: append repos// to the dodder XDG category dirs so a named repo's whole metadata tree nests. Wired into MakeEnvRepo, genesis OnTheFirstDay, and MakeLocalWorkingCopy. - repo_config_cli: -repo_id / DODDER_REPO_ID accept the full grammar. - env_repo genesis: tolerate a pre-existing default blob store config so named repos in one scope share the content-addressed blob pool while keeping independent metadata/index/identity. Prototype limitations (documented): blobs are shared not isolated (madder re-derives the blob-store XDG); empty id stays legacy user scope; only single-dot cwd depth resolves (..name rejected); /name resolves to system scope (remote-first not implemented). Tests: repo_id grammar unit tests; scoped_repos.bats covers two user-scoped named repos isolated, DODDER_REPO_ID addressing, and multi-dot rejection. Docs: docs/plans/2026-06-14-fdr-0019-scoped-repos-prototype.md. https://claude.ai/code/session_01KnD7hCXXKPqnX1SG4ofL5S --- ...6-06-14-fdr-0019-scoped-repos-prototype.md | 72 ++++++ go/internal/bravo/env_dir/construction.go | 32 +++ go/internal/bravo/repo_id/main.go | 222 ++++++++++++++++++ go/internal/bravo/repo_id/main_test.go | 98 ++++++++ go/internal/charlie/repo_config_cli/main.go | 13 +- go/internal/foxtrot/env_repo/genesis.go | 9 + .../command_components_dodder/env_repo.go | 16 +- .../command_components_dodder/genesis.go | 15 +- .../local_working_copy.go | 12 + .../current_version/scoped_repos.bats | 76 ++++++ 10 files changed, 557 insertions(+), 8 deletions(-) create mode 100644 docs/plans/2026-06-14-fdr-0019-scoped-repos-prototype.md create mode 100644 go/internal/bravo/repo_id/main.go create mode 100644 go/internal/bravo/repo_id/main_test.go create mode 100644 zz-tests_bats/current_version/scoped_repos.bats diff --git a/docs/plans/2026-06-14-fdr-0019-scoped-repos-prototype.md b/docs/plans/2026-06-14-fdr-0019-scoped-repos-prototype.md new file mode 100644 index 000000000..c01af98a1 --- /dev/null +++ b/docs/plans/2026-06-14-fdr-0019-scoped-repos-prototype.md @@ -0,0 +1,72 @@ +# FDR-0019 Scoped Repo Resolution --- Dodder-Side Prototype + +Status: prototype (dodder-only, ahead of the madder env_dir.RepoId +change FDR-0019 calls for). See +`docs/features/0019-scoped-repo-resolution.md` for the full design. + +## Why a dodder-only prototype + +The full FDR routes the repo-id grammar (name + scope prefix + +dot-depth) and the `repos//` layout through madder's +`env_dir.RepoId`, which dodder consumes. Madder's `RepoId` is still the +FDR-0003 location-only selector (`.`, `/`, empty) and lives in a +separate repo. This prototype implements the user-visible slice +entirely inside dodder so the feature can be exercised before the +madder change lands, then folded onto the real type later. + +## What it does + +- **Grammar.** New `internal/bravo/repo_id.Id` parses the FDR grammar: + `name` (user), `.name` / `..name` (cwd, dot-depth), `//name` + (system), `/name` (system; remote-first not implemented), `~name` + (parse-only user alias), plus the legacy nameless `.` / `/` / empty. + `String()` renders the canonical single-dot form. Covered by + `repo_id` unit tests. +- **Layout.** `env_dir.NestUnderRepoName` appends `repos//` to + the dodder XDG category dirs (data/config/state/cache/runtime), so a + named repo's whole metadata tree --- config-seed, object index, + inventory-list log, lock --- nests automatically. Wired into the + three env-construction sites: `MakeEnvRepo`, `OnTheFirstDay` + (genesis), and `MakeLocalWorkingCopy`. +- **CLI / env.** The global `-repo_id` flag and `DODDER_REPO_ID` accept + the full grammar; `dodder init -repo_id ...` creates a named + repo; reads address it by the same id. +- **Shared blob store.** Genesis tolerates a pre-existing default blob + store config (idempotent write), so named repos in one scope share + madder's content-addressed blob pool while keeping independent + metadata/index/identity. + +## Deliberate limitations (vs. the full FDR) + +- **Blobs are shared, not isolated.** Madder re-derives the blob-store + XDG via `CloneWithUtilityName`, discarding any suffix applied + dodder-side, so `repos//` nesting reaches only the dodder + metadata tree. Full per-repo blob isolation needs the madder change. +- **Empty id = legacy user scope**, not the FDR's "nearest cwd repo on + the walk-up, else user `default`". This keeps every existing + single-repo tree resolving unchanged (no fixture regen). +- **Single-dot cwd depth only.** `..name` (depth > 1) parses but is + rejected at resolution by `Id.CheckPrototypeSupported`. +- **`/name` resolves straight to system scope**; the remote-first + lookup is out of scope (no remote transport yet). +- No migration command, no `info-repo repos` listing, no MCP + `repo_id` dimension --- those layer on once the grammar is real. + +## Path to the full FDR + +1. Land the grammar + `repos//` layout on madder's + `env_dir.RepoId`; `just go/update-flake-input madder`. +2. Replace `repo_id.Id` with the madder type (or have it delegate), + dropping `CheckPrototypeSupported` once the walk-up resolves + multi-dot depth. +3. Nest the blob-store XDG too (full isolation), add + `migrate-repo-layout`, `info-repo repos`, MCP `repo_id` + resource + URI segment, and completions. + +## Tests + +- `internal/bravo/repo_id/main_test.go` --- grammar parse/canonical + round-trip, error cases, depth gate. +- `zz-tests_bats/current_version/scoped_repos.bats` --- two + user-scoped named repos isolated, `DODDER_REPO_ID` addressing, + multi-dot rejection. diff --git a/go/internal/bravo/env_dir/construction.go b/go/internal/bravo/env_dir/construction.go index 99aa4ddd4..f1fec06fc 100644 --- a/go/internal/bravo/env_dir/construction.go +++ b/go/internal/bravo/env_dir/construction.go @@ -1,6 +1,8 @@ package env_dir import ( + "path/filepath" + "code.linenisgreat.com/dodder/go/internal/0/dodder_env" mad_env_dir "github.com/amarbel-llc/madder/go/pkgs/env_dir" "github.com/amarbel-llc/purse-first/libs/dewey/pkgs/debug" @@ -86,6 +88,36 @@ func MakeDefaultAndInitialize( ) } +// NestUnderRepoName returns a copy of env whose own XDG category dirs +// (data / config / state / cache / runtime) are nested under +// repos//, so several named repos coexist per scope under the +// FDR-0019 layout. The repo's whole metadata tree — config-seed, +// object index, inventory-list log, lock — follows automatically +// because every dodder path is built from these XDG bases. +// +// The blob-store XDG is deliberately NOT nested: madder re-derives it +// via xdg.CloneWithUtilityName, which would discard any suffix applied +// here. In this prototype named repos therefore share the +// content-addressed blob pool while keeping fully independent +// metadata, index, and identity. Full blob isolation waits on the +// madder env_dir.RepoId change. +func NestUnderRepoName( + context errors.Context, + env mad_env_dir.Env, + name string, + do debug.Options, +) mad_env_dir.Env { + x := env.GetXDG() + + x.Data.ActualValue = filepath.Join(x.Data.ActualValue, "repos", name) + x.Config.ActualValue = filepath.Join(x.Config.ActualValue, "repos", name) + x.State.ActualValue = filepath.Join(x.State.ActualValue, "repos", name) + x.Cache.ActualValue = filepath.Join(x.Cache.ActualValue, "repos", name) + x.Runtime.ActualValue = filepath.Join(x.Runtime.ActualValue, "repos", name) + + return MakeWithXDG(context, do, x) +} + func MakeWithDefaultHome( context errors.Context, utilityName string, diff --git a/go/internal/bravo/repo_id/main.go b/go/internal/bravo/repo_id/main.go new file mode 100644 index 000000000..1d9718de5 --- /dev/null +++ b/go/internal/bravo/repo_id/main.go @@ -0,0 +1,222 @@ +// Package repo_id implements dodder's repo *location* selector +// (FDR-0019, "Scoped Repo Resolution"). +// +// It extends the legacy location-only selector (`.`, `/`, empty — +// FDR-0003) with an optional name and a cwd dot-depth, mirroring +// madder's blob_store_id grammar so several named repos can coexist +// per scope under a repos// layout. +// +// This is the dodder-side prototype that lands ahead of the eventual +// madder env_dir.RepoId grammar change the FDR calls for. Two +// deliberate prototype limitations: +// +// - Only single-dot cwd depth resolves. `..name` (depth > 0) parses +// but callers reject it at resolution time. +// - `/name` resolves to the system scope; the FDR's remote-first +// lookup (try a defined remote, then fall back to system) is not +// implemented, so `/name` and `//name` behave identically here. +// +// This selector is distinct from ids.RepoId (the repo *object* genre), +// which the FDR leaves untouched. +package repo_id + +import ( + "strings" + + mad_env_dir "github.com/amarbel-llc/madder/go/pkgs/env_dir" + "github.com/amarbel-llc/purse-first/libs/dewey/pkgs/errors" +) + +// Scope is the location scope a repo id resolves against. +type Scope int + +const ( + // ScopeUser is the XDG user data tree (unprefixed name, or empty). + ScopeUser Scope = iota + // ScopeCwd is the nearest ancestor `.dodder/` tree (`.name`). + ScopeCwd + // ScopeSystem is the XDG system data tree (`//name`, or `/name`). + ScopeSystem +) + +// Id is dodder's repo location selector. The zero value is the empty +// (auto) id. +type Id struct { + isSet bool + scope Scope + name string + cwdDepth uint +} + +// Set parses the repo-id grammar. It satisfies flag.Value via a +// pointer receiver. +func (id *Id) Set(value string) (err error) { + *id = Id{} + + switch value { + case "": + // Auto: nearest cwd repo on the walk-up, else user `default`. + // The prototype treats empty as the legacy user scope so all + // existing single-repo trees keep resolving unchanged. + return nil + + case ".": + id.isSet = true + id.scope = ScopeCwd + return nil + + case "/": + id.isSet = true + id.scope = ScopeSystem + return nil + } + + id.isSet = true + + switch { + case strings.HasPrefix(value, "//"): + id.scope = ScopeSystem + id.name = value[2:] + + case value[0] == '/': + // FDR-0019 reserves `/name` for remote-first selection. The + // remote transport is out of scope, so the prototype resolves + // it straight to the system scope. + id.scope = ScopeSystem + id.name = value[1:] + + case value[0] == '.': + dots := 0 + for dots < len(value) && value[dots] == '.' { + dots++ + } + + if dots == len(value) { + err = errors.Errorf("repo_id is all dots, no name: %q", value) + return err + } + + id.scope = ScopeCwd + id.cwdDepth = uint(dots - 1) + id.name = value[dots:] + + case value[0] == '~': + // `~name` is the parse-only user-scope alias; never emitted. + id.scope = ScopeUser + id.name = value[1:] + + default: + id.scope = ScopeUser + id.name = value + } + + if err = validateName(id.name); err != nil { + err = errors.Wrapf(err, "repo_id: %q", value) + return err + } + + return err +} + +// String renders the canonical form: cwd ids collapse to a single dot +// (depth dropped, #145 precedent), system named ids emit `//name`, and +// user ids emit the bare name with no prefix. +func (id Id) String() string { + if !id.isSet { + return "" + } + + switch id.scope { + case ScopeCwd: + if id.name == "" { + return "." + } + return "." + id.name + + case ScopeSystem: + if id.name == "" { + return "/" + } + return "//" + id.name + + default: + return id.name + } +} + +// IsEmpty reports whether nothing was selected (the auto id). Legacy +// nameless `.` / `/` are NOT empty — they pin a scope. +func (id Id) IsEmpty() bool { + return !id.isSet +} + +func (id Id) IsCwd() bool { + return id.isSet && id.scope == ScopeCwd +} + +func (id Id) IsSystem() bool { + return id.isSet && id.scope == ScopeSystem +} + +// GetName returns the name portion ("" for the legacy nameless and +// auto forms). A non-empty name triggers the repos// nesting. +func (id Id) GetName() string { + return id.name +} + +// GetCwdDepth returns the 0-indexed ancestor depth for cwd-scoped ids +// (0 = single dot = nearest). +func (id Id) GetCwdDepth() uint { + return id.cwdDepth +} + +// GetMad projects this id onto the legacy madder env_dir.RepoId so the +// existing scope routing (MakeDefaultAndInitialize) keeps working. The +// name is carried separately and applied as repos// nesting. +func (id Id) GetMad() mad_env_dir.RepoId { + var mad mad_env_dir.RepoId + + switch { + case id.IsCwd(): + _ = mad.Set(".") + case id.IsSystem(): + _ = mad.Set("/") + } + + return mad +} + +// CheckPrototypeSupported rejects the grammar this dodder-side +// prototype does not yet resolve (multi-dot cwd depth). It is removed +// once the madder env_dir.RepoId change lands the full walk-up. +func (id Id) CheckPrototypeSupported() (err error) { + if id.cwdDepth > 0 { + err = errors.Errorf( + "repo_id %q: cwd dot-depth > 1 is not yet implemented", + id.String(), + ) + return err + } + + return err +} + +func validateName(name string) (err error) { + for _, r := range name { + switch { + case r >= 'a' && r <= 'z', + r >= 'A' && r <= 'Z', + r >= '0' && r <= '9', + r == '_', + r == '-': + default: + err = errors.Errorf( + "name may contain only [a-zA-Z0-9_-]; got %q", + string(r), + ) + return err + } + } + + return err +} diff --git a/go/internal/bravo/repo_id/main_test.go b/go/internal/bravo/repo_id/main_test.go new file mode 100644 index 000000000..9f04d7d6f --- /dev/null +++ b/go/internal/bravo/repo_id/main_test.go @@ -0,0 +1,98 @@ +package repo_id + +import ( + "testing" + + "github.com/amarbel-llc/purse-first/libs/dewey/pkgs/ui" +) + +func TestParse(t1 *testing.T) { + t := ui.MakeT(t1) + + type expectation struct { + input string + empty bool + cwd bool + system bool + name string + cwdDepth uint + canonical string + } + + expectations := []expectation{ + {input: "", empty: true, canonical: ""}, + {input: ".", cwd: true, name: "", canonical: "."}, + {input: "/", system: true, name: "", canonical: "/"}, + {input: "work", name: "work", canonical: "work"}, + {input: "~work", name: "work", canonical: "work"}, + {input: ".notes", cwd: true, name: "notes", canonical: ".notes"}, + {input: "..notes", cwd: true, name: "notes", cwdDepth: 1, canonical: ".notes"}, + {input: "//backup", system: true, name: "backup", canonical: "//backup"}, + {input: "/backup", system: true, name: "backup", canonical: "//backup"}, + } + + for _, e := range expectations { + var id Id + + if err := id.Set(e.input); err != nil { + t.Errorf("Set(%q) returned error: %s", e.input, err) + continue + } + + if id.IsEmpty() != e.empty { + t.Errorf("Set(%q): IsEmpty = %v, want %v", e.input, id.IsEmpty(), e.empty) + } + + if id.IsCwd() != e.cwd { + t.Errorf("Set(%q): IsCwd = %v, want %v", e.input, id.IsCwd(), e.cwd) + } + + if id.IsSystem() != e.system { + t.Errorf("Set(%q): IsSystem = %v, want %v", e.input, id.IsSystem(), e.system) + } + + if id.GetName() != e.name { + t.Errorf("Set(%q): GetName = %q, want %q", e.input, id.GetName(), e.name) + } + + if id.GetCwdDepth() != e.cwdDepth { + t.Errorf("Set(%q): GetCwdDepth = %d, want %d", e.input, id.GetCwdDepth(), e.cwdDepth) + } + + if id.String() != e.canonical { + t.Errorf("Set(%q): String = %q, want %q", e.input, id.String(), e.canonical) + } + } +} + +func TestParseErrors(t1 *testing.T) { + t := ui.MakeT(t1) + + for _, input := range []string{"..", "work/sub", "wo rk", "wo@rk"} { + var id Id + + if err := id.Set(input); err == nil { + t.Errorf("Set(%q): expected error, got none", input) + } + } +} + +func TestCheckPrototypeSupported(t1 *testing.T) { + t := ui.MakeT(t1) + + var nearest Id + if err := nearest.Set(".notes"); err != nil { + t.Fatalf("Set(.notes): %s", err) + } + if err := nearest.CheckPrototypeSupported(); err != nil { + t.Errorf(".notes should be supported, got: %s", err) + } + + var deeper Id + if err := deeper.Set("..notes"); err != nil { + t.Fatalf("Set(..notes): %s", err) + } + if err := deeper.CheckPrototypeSupported(); err == nil { + t.Errorf("..notes should be rejected by the prototype") + } +} diff --git a/go/internal/charlie/repo_config_cli/main.go b/go/internal/charlie/repo_config_cli/main.go index 5b662f566..11571c171 100644 --- a/go/internal/charlie/repo_config_cli/main.go +++ b/go/internal/charlie/repo_config_cli/main.go @@ -6,15 +6,15 @@ import ( "code.linenisgreat.com/dodder/go/internal/0/options_print" "code.linenisgreat.com/dodder/go/internal/0/options_tools" "code.linenisgreat.com/dodder/go/internal/bravo/descriptions" + "code.linenisgreat.com/dodder/go/internal/bravo/repo_id" "code.linenisgreat.com/dodder/go/lib/charlie/config_cli" - mad_env_dir "github.com/amarbel-llc/madder/go/pkgs/env_dir" "github.com/amarbel-llc/purse-first/libs/dewey/pkgs/interfaces" ) type Config struct { config_cli.Config BasePath string - RepoId mad_env_dir.RepoId + RepoId repo_id.Id IgnoreHookErrors bool Hooks string @@ -85,7 +85,12 @@ func (config *Config) SetFlagDefinitions(flagSet interfaces.CLIFlagDefinitions) flagSet.Var(&config.Description, "comment", "Comment for inventory list") - flagSet.Var(&config.RepoId, "repo_id", "repo location: . (cwd) or / (system)") + flagSet.Var( + &config.RepoId, + "repo_id", + "repo location: name (user), .name (cwd), //name (system), "+ + "or bare . / for the legacy nameless scopes", + ) } func Default() (config *Config) { @@ -126,7 +131,7 @@ func (config Config) GetIgnoreWorkspace() bool { return config.IgnoreWorkspace } -func (config Config) GetRepoId() mad_env_dir.RepoId { +func (config Config) GetRepoId() repo_id.Id { return config.RepoId } diff --git a/go/internal/foxtrot/env_repo/genesis.go b/go/internal/foxtrot/env_repo/genesis.go index b915f83a6..9a30e704e 100644 --- a/go/internal/foxtrot/env_repo/genesis.go +++ b/go/internal/foxtrot/env_repo/genesis.go @@ -188,6 +188,15 @@ func (env *Env) writeBlobStoreConfigIfNecessary( directoryLayout, ).GetConfig() + // FDR-0019: named repos share the default (madder) blob store, so + // the config may already exist when a second named repo is + // initialized in the same scope. Reuse it rather than failing the + // exclusive write below. Legacy single-repo inits never hit this + // path — their blob store tree is unique per repo. + if files.Exists(blobStoreConfigPath) { + return + } + blobStoreConfigDir := filepath.Dir(blobStoreConfigPath) if err := env.MakeDirs(blobStoreConfigDir); err != nil { diff --git a/go/internal/tango/command_components_dodder/env_repo.go b/go/internal/tango/command_components_dodder/env_repo.go index 4dd6fbf6f..45899f6fe 100644 --- a/go/internal/tango/command_components_dodder/env_repo.go +++ b/go/internal/tango/command_components_dodder/env_repo.go @@ -25,19 +25,24 @@ func (cmd EnvRepo) MakeEnvRepo( ) env_repo.Env { config := repo_config_cli.FromAny(req.Utility.GetConfigAny()) + if err := config.RepoId.CheckPrototypeSupported(); err != nil { + req.Cancel(err) + } + var ownDir, madderDir mad_env_dir.Env if config.RepoId.IsCwd() || config.RepoId.IsSystem() { + madRepoId := config.RepoId.GetMad() ownDir = env_dir.MakeDefaultAndInitialize( req, dodder_env.XDGUtilityName, config.Debug, - config.RepoId, + madRepoId, ) madderDir = env_dir.MakeDefaultAndInitialize( req, XDGUtilityNameMadder, config.Debug, - config.RepoId, + madRepoId, ) } else { ownDir = env_dir.MakeDefault( @@ -52,6 +57,13 @@ func (cmd EnvRepo) MakeEnvRepo( ) } + // FDR-0019: nest the dodder metadata tree under repos// so + // named repos coexist per scope. The blob store stays shared (see + // env_dir.NestUnderRepoName). + if name := config.RepoId.GetName(); name != "" { + ownDir = env_dir.NestUnderRepoName(req, ownDir, name, config.Debug) + } + envUI := env_ui.Make( req, config, diff --git a/go/internal/tango/command_components_dodder/genesis.go b/go/internal/tango/command_components_dodder/genesis.go index 35952c26e..774d2c283 100644 --- a/go/internal/tango/command_components_dodder/genesis.go +++ b/go/internal/tango/command_components_dodder/genesis.go @@ -116,20 +116,31 @@ func (cmd Genesis) OnTheFirstDay( cmd.GenesisConfig.Blob.SetRepoId(repoId) + if err := config.RepoId.CheckPrototypeSupported(); err != nil { + envUI.Cancel(err) + } + + madRepoId := config.RepoId.GetMad() + ownDir := env_dir.MakeDefaultAndInitialize( req, dodder_env.XDGUtilityName, config.Debug, - config.RepoId, + madRepoId, ) madderDir := env_dir.MakeDefaultAndInitialize( req, XDGUtilityNameMadder, config.Debug, - config.RepoId, + madRepoId, ) + // FDR-0019: nest the dodder metadata tree under repos//. + if name := config.RepoId.GetName(); name != "" { + ownDir = env_dir.NestUnderRepoName(req, ownDir, name, config.Debug) + } + var envRepo env_repo.Env options := env_repo.Options{ diff --git a/go/internal/tango/command_components_dodder/local_working_copy.go b/go/internal/tango/command_components_dodder/local_working_copy.go index 528df0b25..f01a73f5b 100644 --- a/go/internal/tango/command_components_dodder/local_working_copy.go +++ b/go/internal/tango/command_components_dodder/local_working_copy.go @@ -40,6 +40,10 @@ func (cmd LocalWorkingCopy) MakeLocalWorkingCopyWithOptions( ) *local_working_copy.Repo { config := repo_config_cli.FromAny(req.Utility.GetConfigAny()) + if err := config.RepoId.CheckPrototypeSupported(); err != nil { + req.Cancel(err) + } + ownDir := env_dir.MakeDefault( req, dodder_env.XDGUtilityName, @@ -52,6 +56,14 @@ func (cmd LocalWorkingCopy) MakeLocalWorkingCopyWithOptions( config.Debug, ) + // FDR-0019: nest the dodder metadata tree under repos// so + // reads resolve the same named repo that genesis wrote. The cwd / + // user scope itself is still selected by env_dir's walk-up as + // before; only the repos// suffix is layered on here. + if name := config.RepoId.GetName(); name != "" { + ownDir = env_dir.NestUnderRepoName(req, ownDir, name, config.Debug) + } + if envOptions.CustomOut == nil && config.CustomOut != nil { envOptions.CustomOut = config.CustomOut } diff --git a/zz-tests_bats/current_version/scoped_repos.bats b/zz-tests_bats/current_version/scoped_repos.bats new file mode 100644 index 000000000..17d5dde65 --- /dev/null +++ b/zz-tests_bats/current_version/scoped_repos.bats @@ -0,0 +1,76 @@ +#! /usr/bin/env bats + +setup() { + load "$(dirname "$BATS_TEST_FILE")/../lib/common.bash" + + # for shellcheck SC2154 + export output +} + +teardown() { + chflags_nouchg +} + +# bats file_tags=user_story:scoped_repos + +# FDR-0019 prototype: several user-scoped named repos coexist under +# $XDG_DATA_HOME/dodder/repos// and are addressed by the name +# portion of -repo_id. Each repo has an independent metadata tree, so +# an object committed to one is invisible to the other. + +# bats test_tags=user_story:scoped_repos,isolation +function scoped_user_repos_are_isolated { # @test + run_dodder init -yin <(cat_yin) -yang <(cat_yang) -repo_id work work-id + assert_success + + run_dodder init -yin <(cat_yin) -yang <(cat_yang) -repo_id personal personal-id + assert_success + + # a zettel committed to "work" + run_dodder new -repo_id work -edit=false + assert_success + assert_output - <<-EOM + [one/uno !md] + EOM + + # ...is visible in "work" + run_dodder show -repo_id work :z + assert_success + assert_output - <<-EOM + [one/uno !md] + EOM + + # ...and absent from "personal" + run_dodder show -repo_id personal :z + assert_success + assert_output '' + + # each repo reports its own genesis id + run_dodder info-repo -repo_id work id + assert_success + assert_output 'work-id' + + run_dodder info-repo -repo_id personal id + assert_success + assert_output 'personal-id' +} + +# bats test_tags=user_story:scoped_repos,env_var +function scoped_repo_addressed_via_env_var { # @test + run_dodder init -yin <(cat_yin) -yang <(cat_yang) -repo_id work work-id + assert_success + + DODDER_REPO_ID=work run_dodder info-repo id + assert_success + assert_output 'work-id' +} + +# bats test_tags=user_story:scoped_repos,prototype_limit +function scoped_repo_rejects_multi_dot_depth { # @test + run_dodder init -yin <(cat_yin) -yang <(cat_yang) -repo_id work work-id + assert_success + + run_dodder show -repo_id ..notes :z + assert_failure + assert_output --regexp 'cwd dot-depth > 1 is not yet implemented' +}