refactor: rename ensemble to repertoire across codebase

This commit is contained in:
nrslib 2026-02-22 10:50:50 +09:00
parent a59ad1d808
commit c630d78806
56 changed files with 665 additions and 658 deletions

View File

@ -30,7 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Internal
- Comprehensive ensemble test suite: atomic-update, ensemble-paths, file-filter, github-ref-resolver, github-spec, list, lock-file, pack-summary, package-facet-resolution, remove-reference-check, remove, takt-pack-config, tar-parser, takt-pack-schema
- Comprehensive ensemble test suite: atomic-update, ensemble-paths, file-filter, github-ref-resolver, github-spec, list, lock-file, pack-summary, package-facet-resolution, remove-reference-check, remove, takt-ensemble-config, tar-parser, takt-ensemble-schema
- Added `src/faceted-prompting/scope.ts` for @scope reference parsing, validation, and resolution
- Added scope-ref tests for the faceted-prompting module
- Added `inputWait.ts` for shared input-wait state to suppress worker pool log noise

View File

@ -10,10 +10,10 @@
### Added
- **Ensemble パッケージシステム** (`takt ensemble add/remove/list`): GitHub から外部 TAKT パッケージをインポート・管理 — `takt ensemble add github:{owner}/{repo}@{ref}` でパッケージを `~/.takt/ensemble/` にダウンロード。アトミックなインストール、バージョン互換チェック、ロックファイル生成、確認前のパッケージ内容サマリ表示に対応
- **@scope 参照**: piece YAML のファセット参照で `@{owner}/{repo}/{facet-name}` 構文をサポート — インストール済み ensemble パッケージのファセットを直接参照可能(例: `persona: @nrslib/takt-fullstack/expert-coder`
- **4層ファセット解決**: 3層project → user → builtinから4層package-local → project → user → builtinに拡張 — ensemble パッケージのピースは自パッケージ内のファセットを最優先で解決
- **ピース選択に ensemble カテゴリ追加**: インストール済みの ensemble パッケージがピース選択 UI の「ensemble」カテゴリにサブカテゴリとして自動表示
- **Repertoire パッケージシステム** (`takt repertoire add/remove/list`): GitHub から外部 TAKT パッケージをインポート・管理 — `takt repertoire add github:{owner}/{repo}@{ref}` でパッケージを `~/.takt/repertoire/` にダウンロード。アトミックなインストール、バージョン互換チェック、ロックファイル生成、確認前のパッケージ内容サマリ表示に対応
- **@scope 参照**: piece YAML のファセット参照で `@{owner}/{repo}/{facet-name}` 構文をサポート — インストール済み repertoire パッケージのファセットを直接参照可能(例: `persona: @nrslib/takt-fullstack/expert-coder`
- **4層ファセット解決**: 3層project → user → builtinから4層package-local → project → user → builtinに拡張 — repertoire パッケージのピースは自パッケージ内のファセットを最優先で解決
- **ピース選択に repertoire カテゴリ追加**: インストール済みの repertoire パッケージがピース選択 UI の「repertoire」カテゴリにサブカテゴリとして自動表示
- **implement/fix インストラクションにビルドゲート追加**: `implement``fix` のビルトインインストラクションでテスト実行前にビルド(型チェック)の実行を必須化
### Changed
@ -22,15 +22,15 @@
### Fixed
- オーバーライドピースの検証が ensemble スコープを含むリゾルバー経由で実行されるよう修正
- オーバーライドピースの検証が repertoire スコープを含むリゾルバー経由で実行されるよう修正
- `takt export-cc` が新しい `builtins/{lang}/facets/` ディレクトリ構造からファセットを読み込むよう修正
- `confirm()` プロンプトがパイプ経由の stdin に対応(例: `echo "y" | takt ensemble add ...`
- `confirm()` プロンプトがパイプ経由の stdin に対応(例: `echo "y" | takt repertoire add ...`
- イテレーション入力待ち中の `poll_tick` デバッグログ連続出力を抑制
- ピースリゾルバーの `stat()` 呼び出しでアクセス不能エントリ時にクラッシュせずエラーハンドリング
### Internal
- Ensemble テストスイート: atomic-update, ensemble-paths, file-filter, github-ref-resolver, github-spec, list, lock-file, pack-summary, package-facet-resolution, remove-reference-check, remove, takt-pack-config, tar-parser, takt-pack-schema
- Repertoire テストスイート: atomic-update, repertoire-paths, file-filter, github-ref-resolver, github-spec, list, lock-file, pack-summary, package-facet-resolution, remove-reference-check, remove, takt-repertoire-config, tar-parser, takt-repertoire-schema
- `src/faceted-prompting/scope.ts` を追加(@scope 参照のパース・バリデーション・解決)
- faceted-prompting モジュールの scope-ref テストを追加
- `inputWait.ts` を追加(ワーカープールのログノイズ抑制のための入力待ち状態共有)

View File

@ -156,7 +156,7 @@ movements:
| `takt #N` | GitHub Issue をタスクとして実行します |
| `takt switch` | 使う piece を切り替えます |
| `takt eject` | ビルトインの piece/persona をコピーしてカスタマイズできます |
| `takt ensemble add` | GitHub から ensemble パッケージをインストールします |
| `takt repertoire add` | GitHub から repertoire パッケージをインストールします |
全コマンド・オプションは [CLI Reference](./cli-reference.ja.md) を参照してください。
@ -225,7 +225,7 @@ takt --pipeline --task "バグを修正して" --auto-pr
├── config.yaml # プロバイダー、モデル、言語など
├── pieces/ # ユーザー定義の piece
├── facets/ # ユーザー定義のファセットpersonas, policies, knowledge など)
└── ensemble/ # インストール済み ensemble パッケージ
└── repertoire/ # インストール済み repertoire パッケージ
.takt/ # プロジェクトレベル
├── config.yaml # プロジェクト設定
@ -261,7 +261,7 @@ await engine.run();
| [Agent Guide](./agents.md) | カスタムエージェントの設定 |
| [Builtin Catalog](./builtin-catalog.ja.md) | ビルトイン piece・persona の一覧 |
| [Faceted Prompting](./faceted-prompting.ja.md) | プロンプト設計の方法論 |
| [Ensemble Packages](./ensemble.ja.md) | パッケージのインストール・共有 |
| [Repertoire Packages](./repertoire.ja.md) | パッケージのインストール・共有 |
| [Task Management](./task-management.ja.md) | タスクの追加・実行・隔離 |
| [CI/CD Integration](./ci-cd.ja.md) | GitHub Actions・パイプラインモード |
| [Changelog](../CHANGELOG.md) ([日本語](./CHANGELOG.ja.md)) | バージョン履歴 |

View File

@ -300,25 +300,25 @@ takt metrics review
takt metrics review --since 7d
```
### takt ensemble
### takt repertoire
Ensemble パッケージGitHub 上の外部 TAKT パッケージ)を管理します。
Repertoire パッケージGitHub 上の外部 TAKT パッケージ)を管理します。
```bash
# GitHub からパッケージをインストール
takt ensemble add github:{owner}/{repo}@{ref}
takt repertoire add github:{owner}/{repo}@{ref}
# デフォルトブランチからインストール
takt ensemble add github:{owner}/{repo}
takt repertoire add github:{owner}/{repo}
# インストール済みパッケージを一覧表示
takt ensemble list
takt repertoire list
# パッケージを削除
takt ensemble remove @{owner}/{repo}
takt repertoire remove @{owner}/{repo}
```
インストールされたパッケージは `~/.takt/ensemble/` に保存され、ピース選択やファセット解決で利用可能になります。
インストールされたパッケージは `~/.takt/repertoire/` に保存され、ピース選択やファセット解決で利用可能になります。
### takt purge

View File

@ -300,25 +300,25 @@ takt metrics review
takt metrics review --since 7d
```
### takt ensemble
### takt repertoire
Manage ensemble packages (external TAKT packages from GitHub).
Manage repertoire packages (external TAKT packages from GitHub).
```bash
# Install a package from GitHub
takt ensemble add github:{owner}/{repo}@{ref}
takt repertoire add github:{owner}/{repo}@{ref}
# Install from default branch
takt ensemble add github:{owner}/{repo}
takt repertoire add github:{owner}/{repo}
# List installed packages
takt ensemble list
takt repertoire list
# Remove a package
takt ensemble remove @{owner}/{repo}
takt repertoire remove @{owner}/{repo}
```
Installed packages are stored in `~/.takt/ensemble/` and their pieces/facets become available in piece selection and facet resolution.
Installed packages are stored in `~/.takt/repertoire/` and their pieces/facets become available in piece selection and facet resolution.
### takt purge

View File

@ -1,34 +1,34 @@
# Ensemble パッケージ
# Repertoire パッケージ
[English](./ensemble.md)
[English](./repertoire.md)
Ensemble パッケージを使うと、GitHub リポジトリから TAKT のピースやファセットをインストール・共有できます。
Repertoire パッケージを使うと、GitHub リポジトリから TAKT のピースやファセットをインストール・共有できます。
## クイックスタート
```bash
# パッケージをインストール
takt ensemble add github:nrslib/takt-fullstack
takt repertoire add github:nrslib/takt-fullstack
# 特定バージョンを指定してインストール
takt ensemble add github:nrslib/takt-fullstack@v1.0.0
takt repertoire add github:nrslib/takt-fullstack@v1.0.0
# インストール済みパッケージを一覧表示
takt ensemble list
takt repertoire list
# パッケージを削除
takt ensemble remove @nrslib/takt-fullstack
takt repertoire remove @nrslib/takt-fullstack
```
[GitHub CLI](https://cli.github.com/) (`gh`) のインストールと認証が必要です。
## パッケージ構造
TAKT パッケージは `takt-package.yaml` マニフェストとコンテンツディレクトリを持つ GitHub リポジトリです。
TAKT パッケージは `takt-repertoire.yaml` マニフェストとコンテンツディレクトリを持つ GitHub リポジトリです。
```
my-takt-package/
takt-package.yaml # マニフェスト(.takt/takt-package.yaml でも可)
my-takt-repertoire/
takt-repertoire.yaml # マニフェスト(.takt/takt-repertoire.yaml でも可)
facets/
personas/
expert-coder.md
@ -44,7 +44,7 @@ my-takt-package/
`facets/``pieces/` ディレクトリのみがインポートされます。その他のファイルは無視されます。
### takt-package.yaml
### takt-repertoire.yaml
マニフェストは、リポジトリ内のパッケージコンテンツの場所を TAKT に伝えます。
@ -60,7 +60,7 @@ takt:
min_version: 0.22.0
```
マニフェストはリポジトリルート(`takt-package.yaml`)または `.takt/` 内(`.takt/takt-package.yaml`)に配置できます。`.takt/` が優先的に検索されます。
マニフェストはリポジトリルート(`takt-repertoire.yaml`)または `.takt/` 内(`.takt/takt-repertoire.yaml`)に配置できます。`.takt/` が優先的に検索されます。
| フィールド | 必須 | デフォルト | 説明 |
|-----------|------|-----------|------|
@ -71,7 +71,7 @@ takt:
## インストール
```bash
takt ensemble add github:{owner}/{repo}@{ref}
takt repertoire add github:{owner}/{repo}@{ref}
```
`@{ref}` は省略可能です。省略した場合、リポジトリのデフォルトブランチが使用されます。
@ -82,10 +82,10 @@ takt ensemble add github:{owner}/{repo}@{ref}
1. `gh api` 経由で GitHub から tarball をダウンロード
2. `facets/``pieces/` のファイルのみを展開(`.md``.yaml``.yml`
3. `takt-package.yaml` マニフェストをバリデーション
3. `takt-repertoire.yaml` マニフェストをバリデーション
4. TAKT バージョン互換性チェック
5. `~/.takt/ensemble/@{owner}/{repo}/` にファイルをコピー
6. ロックファイル(`.takt-pack-lock.yaml`を生成ソース、ref、コミット SHA
5. `~/.takt/repertoire/@{owner}/{repo}/` にファイルをコピー
6. ロックファイル(`.takt-repertoire-lock.yaml`を生成ソース、ref、コミット SHA
インストールはアトミックに行われます。途中で失敗しても中途半端な状態は残りません。
@ -102,7 +102,7 @@ takt ensemble add github:{owner}/{repo}@{ref}
### ピース
インストールされたピースはピース選択 UI の「ensemble」カテゴリにパッケージごとのサブカテゴリとして表示されます。直接指定も可能です。
インストールされたピースはピース選択 UI の「repertoire」カテゴリにパッケージごとのサブカテゴリとして表示されます。直接指定も可能です。
```bash
takt --piece @nrslib/takt-fullstack/expert
@ -122,9 +122,9 @@ movements:
### 4層ファセット解決
ensemble パッケージのピースが名前(@scope なし)でファセットを解決する場合、次の順序で検索されます。
repertoire パッケージのピースが名前(@scope なし)でファセットを解決する場合、次の順序で検索されます。
1. **パッケージローカル**: `~/.takt/ensemble/@{owner}/{repo}/facets/{type}/`
1. **パッケージローカル**: `~/.takt/repertoire/@{owner}/{repo}/facets/{type}/`
2. **プロジェクト**: `.takt/facets/{type}/`
3. **ユーザー**: `~/.takt/facets/{type}/`
4. **ビルトイン**: `builtins/{lang}/facets/{type}/`
@ -136,7 +136,7 @@ ensemble パッケージのピースが名前(@scope なし)でファセッ
### 一覧表示
```bash
takt ensemble list
takt repertoire list
```
インストール済みパッケージのスコープ、説明、ref、コミット SHA を表示します。
@ -144,21 +144,21 @@ takt ensemble list
### 削除
```bash
takt ensemble remove @{owner}/{repo}
takt repertoire remove @{owner}/{repo}
```
削除前に、ユーザーやプロジェクトのピースがパッケージのファセットを参照していないかチェックし、影響がある場合は警告します。
## ディレクトリ構造
インストールされたパッケージは `~/.takt/ensemble/` に保存されます。
インストールされたパッケージは `~/.takt/repertoire/` に保存されます。
```
~/.takt/ensemble/
~/.takt/repertoire/
@nrslib/
takt-fullstack/
takt-package.yaml # マニフェストのコピー
.takt-pack-lock.yaml # ロックファイルソース、ref、コミット
takt-repertoire.yaml # マニフェストのコピー
.takt-repertoire-lock.yaml # ロックファイルソース、ref、コミット
facets/
personas/
policies/

View File

@ -1,34 +1,34 @@
# Ensemble Packages
# Repertoire Packages
[Japanese](./ensemble.ja.md)
[Japanese](./repertoire.ja.md)
Ensemble packages let you install and share TAKT pieces and facets from GitHub repositories.
Repertoire packages let you install and share TAKT pieces and facets from GitHub repositories.
## Quick Start
```bash
# Install a package
takt ensemble add github:nrslib/takt-fullstack
takt repertoire add github:nrslib/takt-fullstack
# Install a specific version
takt ensemble add github:nrslib/takt-fullstack@v1.0.0
takt repertoire add github:nrslib/takt-fullstack@v1.0.0
# List installed packages
takt ensemble list
takt repertoire list
# Remove a package
takt ensemble remove @nrslib/takt-fullstack
takt repertoire remove @nrslib/takt-fullstack
```
**Requirements:** [GitHub CLI](https://cli.github.com/) (`gh`) must be installed and authenticated.
## Package Structure
A TAKT package is a GitHub repository with a `takt-package.yaml` manifest and content directories:
A TAKT package is a GitHub repository with a `takt-repertoire.yaml` manifest and content directories:
```
my-takt-package/
takt-package.yaml # Package manifest (or .takt/takt-package.yaml)
my-takt-repertoire/
takt-repertoire.yaml # Package manifest (or .takt/takt-repertoire.yaml)
facets/
personas/
expert-coder.md
@ -44,7 +44,7 @@ my-takt-package/
Only `facets/` and `pieces/` directories are imported. Other files are ignored.
### takt-package.yaml
### takt-repertoire.yaml
The manifest tells TAKT where to find the package content within the repository.
@ -60,7 +60,7 @@ takt:
min_version: 0.22.0
```
The manifest can be placed at the repository root (`takt-package.yaml`) or inside `.takt/` (`.takt/takt-package.yaml`). The `.takt/` location is checked first.
The manifest can be placed at the repository root (`takt-repertoire.yaml`) or inside `.takt/` (`.takt/takt-repertoire.yaml`). The `.takt/` location is checked first.
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
@ -71,7 +71,7 @@ The manifest can be placed at the repository root (`takt-package.yaml`) or insid
## Installation
```bash
takt ensemble add github:{owner}/{repo}@{ref}
takt repertoire add github:{owner}/{repo}@{ref}
```
The `@{ref}` is optional. Without it, the repository's default branch is used.
@ -82,10 +82,10 @@ Before installing, TAKT displays a summary of the package contents (facet counts
1. Downloads the tarball from GitHub via `gh api`
2. Extracts only `facets/` and `pieces/` files (`.md`, `.yaml`, `.yml`)
3. Validates the `takt-package.yaml` manifest
3. Validates the `takt-repertoire.yaml` manifest
4. Checks TAKT version compatibility
5. Copies files to `~/.takt/ensemble/@{owner}/{repo}/`
6. Generates a lock file (`.takt-pack-lock.yaml`) with source, ref, and commit SHA
5. Copies files to `~/.takt/repertoire/@{owner}/{repo}/`
6. Generates a lock file (`.takt-repertoire-lock.yaml`) with source, ref, and commit SHA
Installation is atomic — if it fails partway, no partial state is left behind.
@ -102,7 +102,7 @@ Installation is atomic — if it fails partway, no partial state is left behind.
### Pieces
Installed pieces appear in the piece selection UI under the "ensemble" category, organized by package. You can also specify them directly:
Installed pieces appear in the piece selection UI under the "repertoire" category, organized by package. You can also specify them directly:
```bash
takt --piece @nrslib/takt-fullstack/expert
@ -122,9 +122,9 @@ movements:
### 4-layer facet resolution
When a piece from an ensemble package resolves facets by name (without @scope), the resolution order is:
When a piece from a repertoire package resolves facets by name (without @scope), the resolution order is:
1. **Package-local**: `~/.takt/ensemble/@{owner}/{repo}/facets/{type}/`
1. **Package-local**: `~/.takt/repertoire/@{owner}/{repo}/facets/{type}/`
2. **Project**: `.takt/facets/{type}/`
3. **User**: `~/.takt/facets/{type}/`
4. **Builtin**: `builtins/{lang}/facets/{type}/`
@ -136,7 +136,7 @@ This means package pieces automatically find their own facets first, while still
### List
```bash
takt ensemble list
takt repertoire list
```
Shows installed packages with their scope, description, ref, and commit SHA.
@ -144,21 +144,21 @@ Shows installed packages with their scope, description, ref, and commit SHA.
### Remove
```bash
takt ensemble remove @{owner}/{repo}
takt repertoire remove @{owner}/{repo}
```
Before removing, TAKT checks if any user/project pieces reference the package's facets and warns about potential breakage.
## Directory Structure
Installed packages are stored under `~/.takt/ensemble/`:
Installed packages are stored under `~/.takt/repertoire/`:
```
~/.takt/ensemble/
~/.takt/repertoire/
@nrslib/
takt-fullstack/
takt-package.yaml # Copy of the manifest
.takt-pack-lock.yaml # Lock file (source, ref, commit)
takt-repertoire.yaml # Copy of the manifest
.takt-repertoire-lock.yaml # Lock file (source, ref, commit)
facets/
personas/
policies/

View File

@ -124,13 +124,13 @@ describe('E2E: Piece selection branch coverage', () => {
expect(result.stdout).toContain('Piece completed');
}, 240_000);
it('should execute when --piece is an ensemble @scope name (resolver hit branch)', () => {
const pkgRoot = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-packages');
it('should execute when --piece is a repertoire @scope name (resolver hit branch)', () => {
const pkgRoot = join(isolatedEnv.taktDir, 'repertoire', '@nrslib', 'takt-ensembles');
writeAgent(pkgRoot);
writeMinimalPiece(join(pkgRoot, 'pieces', 'critical-thinking.yaml'));
const result = runTaskWithPiece({
piece: '@nrslib/takt-packages/critical-thinking',
piece: '@nrslib/takt-ensembles/critical-thinking',
cwd: testRepo.path,
env: isolatedEnv.env,
});
@ -142,13 +142,13 @@ describe('E2E: Piece selection branch coverage', () => {
it('should fail fast with message when --piece is unknown (resolver miss branch)', () => {
const result = runTaskWithPiece({
piece: '@nrslib/takt-packages/not-found',
piece: '@nrslib/takt-ensembles/not-found',
cwd: testRepo.path,
env: isolatedEnv.env,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Piece not found: @nrslib/takt-packages/not-found');
expect(result.stdout).toContain('Piece not found: @nrslib/takt-ensembles/not-found');
expect(result.stdout).toContain('Cancelled');
}, 240_000);

View File

@ -39,9 +39,9 @@ function readYamlFile<T>(path: string): T {
return parseYaml(raw) as T;
}
const FIXTURE_REPO = 'nrslib/takt-pack-fixture';
const FIXTURE_REPO_SUBDIR = 'nrslib/takt-pack-fixture-subdir';
const FIXTURE_REPO_FACETS_ONLY = 'nrslib/takt-pack-fixture-facets-only';
const FIXTURE_REPO = 'nrslib/takt-ensemble-fixture';
const FIXTURE_REPO_SUBDIR = 'nrslib/takt-ensemble-fixture-subdir';
const FIXTURE_REPO_FACETS_ONLY = 'nrslib/takt-ensemble-fixture-facets-only';
const MISSING_MANIFEST_REPO = 'nrslib/takt';
const FIXTURE_REF = 'v1.0.0';
@ -50,7 +50,7 @@ const canUseSubdirRepo = canAccessRepo(FIXTURE_REPO_SUBDIR) && canAccessRepoRef(
const canUseFacetsOnlyRepo = canAccessRepo(FIXTURE_REPO_FACETS_ONLY) && canAccessRepoRef(FIXTURE_REPO_FACETS_ONLY, FIXTURE_REF);
const canUseMissingManifestRepo = canAccessRepo(MISSING_MANIFEST_REPO);
describe('E2E: takt ensemble (real GitHub fixtures)', () => {
describe('E2E: takt repertoire (real GitHub fixtures)', () => {
let isolatedEnv: IsolatedEnv;
beforeEach(() => {
@ -67,7 +67,7 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
it.skipIf(!canUseFixtureRepo)('should install fixture package from GitHub and create lock file', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
@ -78,14 +78,14 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
expect(result.stdout).toContain(`📦 ${FIXTURE_REPO} @${FIXTURE_REF}`);
expect(result.stdout).toContain('インストールしました');
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture');
expect(existsSync(join(packageDir, 'takt-package.yaml'))).toBe(true);
expect(existsSync(join(packageDir, '.takt-pack-lock.yaml'))).toBe(true);
const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@nrslib', 'takt-ensemble-fixture');
expect(existsSync(join(packageDir, 'takt-repertoire.yaml'))).toBe(true);
expect(existsSync(join(packageDir, '.takt-repertoire-lock.yaml'))).toBe(true);
expect(existsSync(join(packageDir, 'facets'))).toBe(true);
expect(existsSync(join(packageDir, 'pieces'))).toBe(true);
const lock = readYamlFile<LockFile>(join(packageDir, '.takt-pack-lock.yaml'));
expect(lock.source).toBe('github:nrslib/takt-pack-fixture');
const lock = readYamlFile<LockFile>(join(packageDir, '.takt-repertoire-lock.yaml'));
expect(lock.source).toBe('github:nrslib/takt-ensemble-fixture');
expect(lock.ref).toBe(FIXTURE_REF);
expect(lock.commit).toBeTypeOf('string');
expect(lock.commit!.length).toBeGreaterThanOrEqual(7);
@ -94,7 +94,7 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
it.skipIf(!canUseFixtureRepo)('should list installed package after add', () => {
const addResult = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
@ -103,19 +103,19 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
expect(addResult.exitCode).toBe(0);
const listResult = runTakt({
args: ['ensemble', 'list'],
args: ['repertoire', 'list'],
cwd: process.cwd(),
env: isolatedEnv.env,
timeout: 120_000,
});
expect(listResult.exitCode).toBe(0);
expect(listResult.stdout).toContain('@nrslib/takt-pack-fixture');
expect(listResult.stdout).toContain('@nrslib/takt-ensemble-fixture');
}, 240_000);
it.skipIf(!canUseFixtureRepo)('should remove installed package with confirmation', () => {
const addResult = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
@ -124,7 +124,7 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
expect(addResult.exitCode).toBe(0);
const removeResult = runTakt({
args: ['ensemble', 'remove', '@nrslib/takt-pack-fixture'],
args: ['repertoire', 'remove', '@nrslib/takt-ensemble-fixture'],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
@ -132,13 +132,13 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
});
expect(removeResult.exitCode).toBe(0);
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture');
const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@nrslib', 'takt-ensemble-fixture');
expect(existsSync(packageDir)).toBe(false);
}, 240_000);
it.skipIf(!canUseFixtureRepo)('should cancel installation when user answers N', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'n\n',
@ -148,13 +148,13 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('キャンセルしました');
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture');
const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@nrslib', 'takt-ensemble-fixture');
expect(existsSync(packageDir)).toBe(false);
}, 240_000);
it.skipIf(!canUseSubdirRepo)('should install subdir fixture package', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO_SUBDIR}@${FIXTURE_REF}`],
args: ['repertoire', 'add', `github:${FIXTURE_REPO_SUBDIR}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
@ -162,15 +162,15 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
});
expect(result.exitCode).toBe(0);
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture-subdir');
expect(existsSync(join(packageDir, 'takt-package.yaml'))).toBe(true);
expect(existsSync(join(packageDir, '.takt-pack-lock.yaml'))).toBe(true);
const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@nrslib', 'takt-ensemble-fixture-subdir');
expect(existsSync(join(packageDir, 'takt-repertoire.yaml'))).toBe(true);
expect(existsSync(join(packageDir, '.takt-repertoire-lock.yaml'))).toBe(true);
expect(existsSync(join(packageDir, 'facets')) || existsSync(join(packageDir, 'pieces'))).toBe(true);
}, 240_000);
it.skipIf(!canUseFacetsOnlyRepo)('should install facets-only fixture package without requiring pieces directory', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO_FACETS_ONLY}@${FIXTURE_REF}`],
args: ['repertoire', 'add', `github:${FIXTURE_REPO_FACETS_ONLY}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
@ -178,14 +178,14 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
});
expect(result.exitCode).toBe(0);
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture-facets-only');
const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@nrslib', 'takt-ensemble-fixture-facets-only');
expect(existsSync(join(packageDir, 'facets'))).toBe(true);
expect(existsSync(join(packageDir, 'pieces'))).toBe(false);
}, 240_000);
it.skipIf(!canUseMissingManifestRepo)('should fail when repository has no takt-package.yaml', () => {
it.skipIf(!canUseMissingManifestRepo)('should fail when repository has no takt-repertoire.yaml', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${MISSING_MANIFEST_REPO}`],
args: ['repertoire', 'add', `github:${MISSING_MANIFEST_REPO}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
@ -193,6 +193,6 @@ describe('E2E: takt ensemble (real GitHub fixtures)', () => {
});
expect(result.exitCode).not.toBe(0);
expect(result.stdout).toContain('takt-package.yaml not found');
expect(result.stdout).toContain('takt-repertoire.yaml not found');
}, 240_000);
});

View File

@ -1,83 +1,83 @@
/**
* E2E tests for `takt ensemble` subcommands.
* E2E tests for `takt repertoire` subcommands.
*
* All tests are marked as `it.todo()` because the `takt ensemble` command
* All tests are marked as `it.todo()` because the `takt repertoire` command
* is not yet implemented. These serve as the specification skeleton;
* fill in the callbacks when the implementation lands.
*
* GitHub fixture repos used:
* - github:nrslib/takt-pack-fixture (standard: facets/ + pieces/)
* - github:nrslib/takt-pack-fixture-subdir (path field specified)
* - github:nrslib/takt-pack-fixture-facets-only (facets only, no pieces/)
* - github:nrslib/takt-ensemble-fixture (standard: facets/ + pieces/)
* - github:nrslib/takt-ensemble-fixture-subdir (path field specified)
* - github:nrslib/takt-ensemble-fixture-facets-only (facets only, no pieces/)
*
*/
import { describe, it } from 'vitest';
// ---------------------------------------------------------------------------
// E2E: takt ensemble add — 正常系
// E2E: takt repertoire add — 正常系
// ---------------------------------------------------------------------------
describe('E2E: takt ensemble add (正常系)', () => {
describe('E2E: takt repertoire add (正常系)', () => {
// E1: 標準パッケージのインポート
// Given: 空の isolatedEnv
// When: takt ensemble add github:nrslib/takt-pack-fixture@v1.0.0、y 入力
// Then: {taktDir}/ensemble/@nrslib/takt-pack-fixture/ に takt-pack.yaml,
// .takt-pack-lock.yaml, facets/, pieces/ が存在する
// When: takt repertoire add github:nrslib/takt-ensemble-fixture@v1.0.0、y 入力
// Then: {taktDir}/repertoire/@nrslib/takt-ensemble-fixture/ に takt-repertoire.yaml,
// .takt-repertoire-lock.yaml, facets/, pieces/ が存在する
it.todo('should install standard package and verify directory structure');
// E2: lock ファイルのフィールド確認
// Given: E1 完了後
// When: .takt-pack-lock.yaml を読む
// When: .takt-repertoire-lock.yaml を読む
// Then: source, ref, commit, imported_at フィールドがすべて存在する
it.todo('should generate .takt-pack-lock.yaml with source, ref, commit, imported_at');
it.todo('should generate .takt-repertoire-lock.yaml with source, ref, commit, imported_at');
// E3: サブディレクトリ型パッケージのインポート
// Given: 空の isolatedEnv
// When: takt ensemble add github:nrslib/takt-pack-fixture-subdir@v1.0.0、y 入力
// When: takt repertoire add github:nrslib/takt-ensemble-fixture-subdir@v1.0.0、y 入力
// Then: path フィールドで指定されたサブディレクトリ配下のファイルのみコピーされる
it.todo('should install subdir-type package and copy only path-specified files');
// E4: ファセットのみパッケージのインポート
// Given: 空の isolatedEnv
// When: takt ensemble add github:nrslib/takt-pack-fixture-facets-only@v1.0.0、y 入力
// When: takt repertoire add github:nrslib/takt-ensemble-fixture-facets-only@v1.0.0、y 入力
// Then: facets/ は存在し、pieces/ ディレクトリは存在しない
it.todo('should install facets-only package without creating pieces/ directory');
// E4b: コミットSHA指定
// Given: 空の isolatedEnv
// When: takt ensemble add github:nrslib/takt-pack-fixture@{sha}、y 入力
// Then: .takt-pack-lock.yaml の commit フィールドが指定した SHA と一致する
// When: takt repertoire add github:nrslib/takt-ensemble-fixture@{sha}、y 入力
// Then: .takt-repertoire-lock.yaml の commit フィールドが指定した SHA と一致する
it.todo('should populate lock file commit field with the specified commit SHA when installing by SHA');
// E5: インストール前サマリー表示
// Given: 空の isolatedEnv
// When: takt ensemble add github:nrslib/takt-pack-fixture@v1.0.0、N 入力(確認でキャンセル)
// Then: stdout に "📦 nrslib/takt-pack-fixture", "faceted:", "pieces:" が含まれる
// When: takt repertoire add github:nrslib/takt-ensemble-fixture@v1.0.0、N 入力(確認でキャンセル)
// Then: stdout に "📦 nrslib/takt-ensemble-fixture", "faceted:", "pieces:" が含まれる
it.todo('should display pre-install summary with package name, faceted count, and pieces list');
// E6: 権限警告表示edit: true ピース)
// Given: edit: true を含むパッケージ
// When: ensemble add、N 入力
// When: repertoire add、N 入力
// Then: stdout に ⚠ が含まれる
it.todo('should display warning symbol when package contains piece with edit: true');
// E7: ユーザー確認 N で中断
// Given: 空の isolatedEnv
// When: ensemble add、N 入力
// When: repertoire add、N 入力
// Then: インストールディレクトリが存在しない。exit code 0
it.todo('should abort installation when user answers N to confirmation prompt');
});
// ---------------------------------------------------------------------------
// E2E: takt ensemble add — 上書きシナリオ
// E2E: takt repertoire add — 上書きシナリオ
// ---------------------------------------------------------------------------
describe('E2E: takt ensemble add (上書きシナリオ)', () => {
describe('E2E: takt repertoire add (上書きシナリオ)', () => {
// E8: 既存パッケージの上書き警告表示
// Given: 1回目インストール済み
// When: 2回目 ensemble add
// Then: stdout に "⚠ パッケージ @nrslib/takt-pack-fixture は既にインストールされています" が含まれる
// When: 2回目 repertoire add
// Then: stdout に "⚠ パッケージ @nrslib/takt-ensemble-fixture は既にインストールされています" が含まれる
it.todo('should display already-installed warning on second add');
// E9: 上書き y で原子的更新
@ -93,80 +93,80 @@ describe('E2E: takt ensemble add (上書きシナリオ)', () => {
it.todo('should keep existing package when user answers N to overwrite prompt');
// E11: 前回異常終了残留物(.tmp/)クリーンアップ
// Given: {ensembleDir}/@nrslib/takt-pack-fixture.tmp/ が既に存在する状態
// When: ensemble add、y 入力
// Given: {repertoireDir}/@nrslib/takt-ensemble-fixture.tmp/ が既に存在する状態
// When: repertoire add、y 入力
// Then: インストールが正常完了する。exit code 0
it.todo('should clean up leftover .tmp/ directory from previous failed installation');
// E12: 前回異常終了残留物(.bak/)クリーンアップ
// Given: {ensembleDir}/@nrslib/takt-pack-fixture.bak/ が既に存在する状態
// When: ensemble add、y 入力
// Given: {repertoireDir}/@nrslib/takt-ensemble-fixture.bak/ が既に存在する状態
// When: repertoire add、y 入力
// Then: インストールが正常完了する。exit code 0
it.todo('should clean up leftover .bak/ directory from previous failed installation');
});
// ---------------------------------------------------------------------------
// E2E: takt ensemble add — バリデーション・エラー系
// E2E: takt repertoire add — バリデーション・エラー系
// ---------------------------------------------------------------------------
describe('E2E: takt ensemble add (バリデーション・エラー系)', () => {
// E13: takt-pack.yaml 不在リポジトリ
// Given: takt-pack.yaml のないリポジトリを指定
// When: ensemble add
describe('E2E: takt repertoire add (バリデーション・エラー系)', () => {
// E13: takt-repertoire.yaml 不在リポジトリ
// Given: takt-repertoire.yaml のないリポジトリを指定
// When: repertoire add
// Then: exit code 非0。エラーメッセージ表示
it.todo('should fail with error when repository has no takt-pack.yaml');
it.todo('should fail with error when repository has no takt-repertoire.yaml');
// E14: path に絶対パス(/foo
// Given: path: /foo の takt-pack.yaml
// When: ensemble add
// Given: path: /foo の takt-repertoire.yaml
// When: repertoire add
// Then: exit code 非0
it.todo('should reject takt-pack.yaml with absolute path in path field (/foo)');
it.todo('should reject takt-repertoire.yaml with absolute path in path field (/foo)');
// E15: path に .. によるリポジトリ外参照
// Given: path: ../outside の takt-pack.yaml
// When: ensemble add
// Given: path: ../outside の takt-repertoire.yaml
// When: repertoire add
// Then: exit code 非0
it.todo('should reject takt-pack.yaml with path traversal via ".." segments');
it.todo('should reject takt-repertoire.yaml with path traversal via ".." segments');
// E16: 空パッケージfacets/ も pieces/ もない)
// Given: facets/, pieces/ のどちらもない takt-pack.yaml
// When: ensemble add
// Given: facets/, pieces/ のどちらもない takt-repertoire.yaml
// When: repertoire add
// Then: exit code 非0
it.todo('should reject package with neither facets/ nor pieces/ directory');
// E17: min_version 不正形式1.0、セグメント不足)
// Given: takt.min_version: "1.0"
// When: ensemble add
// When: repertoire add
// Then: exit code 非0
it.todo('should reject takt-pack.yaml with min_version "1.0" (missing patch segment)');
it.todo('should reject takt-repertoire.yaml with min_version "1.0" (missing patch segment)');
// E18: min_version 不正形式v1.0.0、v プレフィックス)
// Given: takt.min_version: "v1.0.0"
// When: ensemble add
// When: repertoire add
// Then: exit code 非0
it.todo('should reject takt-pack.yaml with min_version "v1.0.0" (v prefix)');
it.todo('should reject takt-repertoire.yaml with min_version "v1.0.0" (v prefix)');
// E19: min_version 不正形式1.0.0-alpha、pre-release
// Given: takt.min_version: "1.0.0-alpha"
// When: ensemble add
// When: repertoire add
// Then: exit code 非0
it.todo('should reject takt-pack.yaml with min_version "1.0.0-alpha" (pre-release suffix)');
it.todo('should reject takt-repertoire.yaml with min_version "1.0.0-alpha" (pre-release suffix)');
// E20: min_version が現在の TAKT より新しい
// Given: takt.min_version: "999.0.0"
// When: ensemble add
// When: repertoire add
// Then: exit code 非0。必要バージョンと現在バージョンが表示される
it.todo('should fail with version mismatch message when min_version exceeds current takt version');
});
// ---------------------------------------------------------------------------
// E2E: takt ensemble remove
// E2E: takt repertoire remove
// ---------------------------------------------------------------------------
describe('E2E: takt ensemble remove', () => {
describe('E2E: takt repertoire remove', () => {
// E21: 正常削除 y
// Given: パッケージインストール済み
// When: takt ensemble remove @nrslib/takt-pack-fixture、y 入力
// When: takt repertoire remove @nrslib/takt-ensemble-fixture、y 入力
// Then: ディレクトリが削除される。@nrslib/ 配下が空なら @nrslib/ も削除
it.todo('should remove installed package directory when user answers y');
@ -196,26 +196,26 @@ describe('E2E: takt ensemble remove', () => {
});
// ---------------------------------------------------------------------------
// E2E: takt ensemble list
// E2E: takt repertoire list
// ---------------------------------------------------------------------------
describe('E2E: takt ensemble list', () => {
describe('E2E: takt repertoire list', () => {
// E26: インストール済みパッケージ一覧表示
// Given: パッケージ1件インストール済み
// When: takt ensemble list
// Then: "📦 インストール済みパッケージ:" と @nrslib/takt-pack-fixture、
// When: takt repertoire list
// Then: "📦 インストール済みパッケージ:" と @nrslib/takt-ensemble-fixture、
// description、ref、commit 先頭7文字が表示される
it.todo('should list installed packages with name, description, ref, and abbreviated commit');
// E27: 空状態での表示
// Given: ensemble/ が空(パッケージなし)
// When: takt ensemble list
// Given: repertoire/ が空(パッケージなし)
// When: takt repertoire list
// Then: パッケージなし相当のメッセージ。exit code 0
it.todo('should display empty-state message when no packages are installed');
// E28: 複数パッケージの一覧
// Given: 2件以上インストール済み
// When: takt ensemble list
// When: takt repertoire list
// Then: すべてのパッケージが表示される
it.todo('should list all installed packages when multiple packages exist');
});

View File

@ -4,7 +4,7 @@
* Covers:
* - isScopeRef(): detects @{owner}/{repo}/{facet-name} format
* - parseScopeRef(): parses components from scope reference
* - resolveScopeRef(): resolves to ~/.takt/ensemble/@{owner}/{repo}/facets/{facet-type}/{facet-name}.md
* - resolveScopeRef(): resolves to ~/.takt/repertoire/@{owner}/{repo}/facets/{facet-type}/{facet-name}.md
* - facet-type mapping from field context (personapersonas, policypolicies, etc.)
* - Name constraint validation (owner, repo, facet-name patterns)
* - Case normalization (uppercase lowercase)
@ -124,89 +124,89 @@ describe('parseScopeRef', () => {
// ---------------------------------------------------------------------------
describe('resolveScopeRef', () => {
let tempEnsembleDir: string;
let tempRepertoireDir: string;
beforeEach(() => {
tempEnsembleDir = mkdtempSync(join(tmpdir(), 'takt-ensemble-'));
tempRepertoireDir = mkdtempSync(join(tmpdir(), 'takt-repertoire-'));
});
afterEach(() => {
rmSync(tempEnsembleDir, { recursive: true, force: true });
rmSync(tempRepertoireDir, { recursive: true, force: true });
});
it('should resolve persona scope ref to facets/personas/{name}.md', () => {
// Given: ensemble directory with the package's persona file
const facetDir = join(tempEnsembleDir, '@nrslib', 'takt-fullstack', 'facets', 'personas');
// Given: repertoire directory with the package's persona file
const facetDir = join(tempRepertoireDir, '@nrslib', 'takt-fullstack', 'facets', 'personas');
mkdirSync(facetDir, { recursive: true });
writeFileSync(join(facetDir, 'expert-coder.md'), 'Expert coder persona');
const scopeRef: ScopeRef = { owner: 'nrslib', repo: 'takt-fullstack', name: 'expert-coder' };
// When: scope ref is resolved with facetType 'personas'
const result = resolveScopeRef(scopeRef, 'personas', tempEnsembleDir);
const result = resolveScopeRef(scopeRef, 'personas', tempRepertoireDir);
// Then: resolved to the correct file path
expect(result).toBe(join(tempEnsembleDir, '@nrslib', 'takt-fullstack', 'facets', 'personas', 'expert-coder.md'));
expect(result).toBe(join(tempRepertoireDir, '@nrslib', 'takt-fullstack', 'facets', 'personas', 'expert-coder.md'));
});
it('should resolve policy scope ref to facets/policies/{name}.md', () => {
// Given: ensemble directory with policy file
const facetDir = join(tempEnsembleDir, '@nrslib', 'takt-fullstack', 'facets', 'policies');
// Given: repertoire directory with policy file
const facetDir = join(tempRepertoireDir, '@nrslib', 'takt-fullstack', 'facets', 'policies');
mkdirSync(facetDir, { recursive: true });
writeFileSync(join(facetDir, 'owasp-checklist.md'), 'OWASP content');
const scopeRef: ScopeRef = { owner: 'nrslib', repo: 'takt-fullstack', name: 'owasp-checklist' };
// When: scope ref is resolved with facetType 'policies'
const result = resolveScopeRef(scopeRef, 'policies', tempEnsembleDir);
const result = resolveScopeRef(scopeRef, 'policies', tempRepertoireDir);
// Then: resolved to correct path
expect(result).toBe(join(tempEnsembleDir, '@nrslib', 'takt-fullstack', 'facets', 'policies', 'owasp-checklist.md'));
expect(result).toBe(join(tempRepertoireDir, '@nrslib', 'takt-fullstack', 'facets', 'policies', 'owasp-checklist.md'));
});
it('should resolve knowledge scope ref to facets/knowledge/{name}.md', () => {
// Given: ensemble directory with knowledge file
const facetDir = join(tempEnsembleDir, '@nrslib', 'takt-security-facets', 'facets', 'knowledge');
// Given: repertoire directory with knowledge file
const facetDir = join(tempRepertoireDir, '@nrslib', 'takt-security-facets', 'facets', 'knowledge');
mkdirSync(facetDir, { recursive: true });
writeFileSync(join(facetDir, 'vulnerability-patterns.md'), 'Vuln patterns');
const scopeRef: ScopeRef = { owner: 'nrslib', repo: 'takt-security-facets', name: 'vulnerability-patterns' };
// When: scope ref is resolved with facetType 'knowledge'
const result = resolveScopeRef(scopeRef, 'knowledge', tempEnsembleDir);
const result = resolveScopeRef(scopeRef, 'knowledge', tempRepertoireDir);
// Then: resolved to correct path
expect(result).toBe(join(tempEnsembleDir, '@nrslib', 'takt-security-facets', 'facets', 'knowledge', 'vulnerability-patterns.md'));
expect(result).toBe(join(tempRepertoireDir, '@nrslib', 'takt-security-facets', 'facets', 'knowledge', 'vulnerability-patterns.md'));
});
it('should resolve instructions scope ref to facets/instructions/{name}.md', () => {
// Given: instruction file
const facetDir = join(tempEnsembleDir, '@acme', 'takt-backend', 'facets', 'instructions');
const facetDir = join(tempRepertoireDir, '@acme', 'takt-backend', 'facets', 'instructions');
mkdirSync(facetDir, { recursive: true });
writeFileSync(join(facetDir, 'review-checklist.md'), 'Review steps');
const scopeRef: ScopeRef = { owner: 'acme', repo: 'takt-backend', name: 'review-checklist' };
// When: scope ref is resolved with facetType 'instructions'
const result = resolveScopeRef(scopeRef, 'instructions', tempEnsembleDir);
const result = resolveScopeRef(scopeRef, 'instructions', tempRepertoireDir);
// Then: correct path
expect(result).toBe(join(tempEnsembleDir, '@acme', 'takt-backend', 'facets', 'instructions', 'review-checklist.md'));
expect(result).toBe(join(tempRepertoireDir, '@acme', 'takt-backend', 'facets', 'instructions', 'review-checklist.md'));
});
it('should resolve output-contracts scope ref to facets/output-contracts/{name}.md', () => {
// Given: output contract file
const facetDir = join(tempEnsembleDir, '@acme', 'takt-backend', 'facets', 'output-contracts');
const facetDir = join(tempRepertoireDir, '@acme', 'takt-backend', 'facets', 'output-contracts');
mkdirSync(facetDir, { recursive: true });
writeFileSync(join(facetDir, 'review-report.md'), 'Report contract');
const scopeRef: ScopeRef = { owner: 'acme', repo: 'takt-backend', name: 'review-report' };
// When: scope ref is resolved with facetType 'output-contracts'
const result = resolveScopeRef(scopeRef, 'output-contracts', tempEnsembleDir);
const result = resolveScopeRef(scopeRef, 'output-contracts', tempRepertoireDir);
// Then: correct path
expect(result).toBe(join(tempEnsembleDir, '@acme', 'takt-backend', 'facets', 'output-contracts', 'review-report.md'));
expect(result).toBe(join(tempRepertoireDir, '@acme', 'takt-backend', 'facets', 'output-contracts', 'review-report.md'));
});
});

View File

@ -1,5 +1,5 @@
import { join } from 'node:path';
import type { ScanConfig } from '../../features/ensemble/remove.js';
import type { ScanConfig } from '../../features/repertoire/remove.js';
/**
* Build a ScanConfig for tests using tempDir as the root.

View File

@ -72,7 +72,7 @@ function writeYaml(path: string, content: string): void {
writeFileSync(path, content.trim() + '\n', 'utf-8');
}
function createPieceMap(entries: { name: string; source: 'builtin' | 'user' | 'project' | 'ensemble' }[]):
function createPieceMap(entries: { name: string; source: 'builtin' | 'user' | 'project' | 'repertoire' }[]):
Map<string, PieceWithSource> {
const pieces = new Map<string, PieceWithSource>();
for (const entry of entries) {
@ -442,11 +442,11 @@ describe('buildCategorizedPieces', () => {
expect(paths).toEqual(['Parent / Child']);
});
it('should append ensemble category for @scope pieces', () => {
it('should append repertoire category for @scope pieces', () => {
const allPieces = createPieceMap([
{ name: 'default', source: 'builtin' },
{ name: '@nrslib/takt-pack/expert', source: 'ensemble' },
{ name: '@nrslib/takt-pack/reviewer', source: 'ensemble' },
{ name: '@nrslib/takt-ensemble/expert', source: 'repertoire' },
{ name: '@nrslib/takt-ensemble/reviewer', source: 'repertoire' },
]);
const config = {
pieceCategories: [{ name: 'Main', pieces: ['default'], children: [] }],
@ -459,21 +459,21 @@ describe('buildCategorizedPieces', () => {
const categorized = buildCategorizedPieces(allPieces, config, process.cwd());
// ensemble category is appended
const ensembleCat = categorized.categories.find((c) => c.name === 'ensemble');
expect(ensembleCat).toBeDefined();
expect(ensembleCat!.children).toHaveLength(1);
expect(ensembleCat!.children[0]!.name).toBe('@nrslib/takt-pack');
expect(ensembleCat!.children[0]!.pieces).toEqual(
expect.arrayContaining(['@nrslib/takt-pack/expert', '@nrslib/takt-pack/reviewer']),
// repertoire category is appended
const repertoireCat = categorized.categories.find((c) => c.name === 'repertoire');
expect(repertoireCat).toBeDefined();
expect(repertoireCat!.children).toHaveLength(1);
expect(repertoireCat!.children[0]!.name).toBe('@nrslib/takt-ensemble');
expect(repertoireCat!.children[0]!.pieces).toEqual(
expect.arrayContaining(['@nrslib/takt-ensemble/expert', '@nrslib/takt-ensemble/reviewer']),
);
// @scope pieces must not appear in Others
const othersCat = categorized.categories.find((c) => c.name === 'Others');
expect(othersCat?.pieces ?? []).not.toContain('@nrslib/takt-pack/expert');
expect(othersCat?.pieces ?? []).not.toContain('@nrslib/takt-ensemble/expert');
});
it('should not append ensemble category when no @scope pieces exist', () => {
it('should not append repertoire category when no @scope pieces exist', () => {
const allPieces = createPieceMap([{ name: 'default', source: 'builtin' }]);
const config = {
pieceCategories: [{ name: 'Main', pieces: ['default'], children: [] }],
@ -486,7 +486,7 @@ describe('buildCategorizedPieces', () => {
const categorized = buildCategorizedPieces(allPieces, config, process.cwd());
const ensembleCat = categorized.categories.find((c) => c.name === 'ensemble');
expect(ensembleCat).toBeUndefined();
const repertoireCat = categorized.categories.find((c) => c.name === 'repertoire');
expect(repertoireCat).toBeUndefined();
});
});

View File

@ -189,7 +189,7 @@ movements:
});
describe('loadPieceByIdentifier with @scope ref (ensemble)', () => {
describe('loadPieceByIdentifier with @scope ref (repertoire)', () => {
let tempDir: string;
let configDir: string;
const originalTaktConfigDir = process.env.TAKT_CONFIG_DIR;
@ -210,14 +210,14 @@ describe('loadPieceByIdentifier with @scope ref (ensemble)', () => {
rmSync(configDir, { recursive: true, force: true });
});
it('should load piece by @scope ref (ensemble)', () => {
// Given: ensemble package with a piece file
const piecesDir = join(configDir, 'ensemble', '@nrslib', 'takt-pack', 'pieces');
it('should load piece by @scope ref (repertoire)', () => {
// Given: repertoire package with a piece file
const piecesDir = join(configDir, 'repertoire', '@nrslib', 'takt-ensemble', 'pieces');
mkdirSync(piecesDir, { recursive: true });
writeFileSync(join(piecesDir, 'expert.yaml'), SAMPLE_PIECE);
// When: piece is loaded via @scope ref
const piece = loadPieceByIdentifier('@nrslib/takt-pack/expert', tempDir);
const piece = loadPieceByIdentifier('@nrslib/takt-ensemble/expert', tempDir);
// Then: the piece is resolved correctly
expect(piece).not.toBeNull();
@ -225,19 +225,19 @@ describe('loadPieceByIdentifier with @scope ref (ensemble)', () => {
});
it('should return null for non-existent @scope piece', () => {
// Given: ensemble dir exists but the requested piece does not
const piecesDir = join(configDir, 'ensemble', '@nrslib', 'takt-pack', 'pieces');
// Given: repertoire dir exists but the requested piece does not
const piecesDir = join(configDir, 'repertoire', '@nrslib', 'takt-ensemble', 'pieces');
mkdirSync(piecesDir, { recursive: true });
// When: a non-existent piece is requested
const piece = loadPieceByIdentifier('@nrslib/takt-pack/no-such-piece', tempDir);
const piece = loadPieceByIdentifier('@nrslib/takt-ensemble/no-such-piece', tempDir);
// Then: null is returned
expect(piece).toBeNull();
});
});
describe('loadAllPiecesWithSources with ensemble pieces', () => {
describe('loadAllPiecesWithSources with repertoire pieces', () => {
let tempDir: string;
let configDir: string;
const originalTaktConfigDir = process.env.TAKT_CONFIG_DIR;
@ -258,28 +258,28 @@ describe('loadAllPiecesWithSources with ensemble pieces', () => {
rmSync(configDir, { recursive: true, force: true });
});
it('should include ensemble pieces with @scope qualified names', () => {
// Given: ensemble package with a piece file
const piecesDir = join(configDir, 'ensemble', '@nrslib', 'takt-pack', 'pieces');
it('should include repertoire pieces with @scope qualified names', () => {
// Given: repertoire package with a piece file
const piecesDir = join(configDir, 'repertoire', '@nrslib', 'takt-ensemble', 'pieces');
mkdirSync(piecesDir, { recursive: true });
writeFileSync(join(piecesDir, 'expert.yaml'), SAMPLE_PIECE);
// When: all pieces are loaded
const pieces = loadAllPiecesWithSources(tempDir);
// Then: the ensemble piece is included with 'ensemble' source
expect(pieces.has('@nrslib/takt-pack/expert')).toBe(true);
expect(pieces.get('@nrslib/takt-pack/expert')!.source).toBe('ensemble');
// Then: the repertoire piece is included with 'repertoire' source
expect(pieces.has('@nrslib/takt-ensemble/expert')).toBe(true);
expect(pieces.get('@nrslib/takt-ensemble/expert')!.source).toBe('repertoire');
});
it('should not throw when ensemble dir does not exist', () => {
// Given: no ensemble dir created (configDir/ensemble does not exist)
it('should not throw when repertoire dir does not exist', () => {
// Given: no repertoire dir created (configDir/repertoire does not exist)
// When: all pieces are loaded
const pieces = loadAllPiecesWithSources(tempDir);
// Then: no @scope pieces are present and no error thrown
const ensemblePieces = Array.from(pieces.keys()).filter((k) => k.startsWith('@'));
expect(ensemblePieces).toHaveLength(0);
const repertoirePieces = Array.from(pieces.keys()).filter((k) => k.startsWith('@'));
expect(repertoirePieces).toHaveLength(0);
});
});

View File

@ -1,7 +1,7 @@
/**
* Unit tests for ensemble atomic installation/update sequence.
* Unit tests for repertoire atomic installation/update sequence.
*
* Target: src/features/ensemble/atomic-update.ts
* Target: src/features/repertoire/atomic-update.ts
*
* Atomic update steps under test:
* Step 0: Clean up leftover .tmp/ and .bak/ from previous failed runs
@ -23,9 +23,9 @@ import {
cleanupResiduals,
atomicReplace,
type AtomicReplaceOptions,
} from '../features/ensemble/atomic-update.js';
} from '../features/repertoire/atomic-update.js';
describe('ensemble atomic install: leftover cleanup (Step 0)', () => {
describe('repertoire atomic install: leftover cleanup (Step 0)', () => {
let tempDir: string;
beforeEach(() => {
@ -69,7 +69,7 @@ describe('ensemble atomic install: leftover cleanup (Step 0)', () => {
});
});
describe('ensemble atomic install: failure recovery', () => {
describe('repertoire atomic install: failure recovery', () => {
let tempDir: string;
beforeEach(() => {

View File

@ -1,7 +1,7 @@
/**
* Unit tests for ensemble reference integrity scanner.
* Unit tests for repertoire reference integrity scanner.
*
* Target: src/features/ensemble/remove.ts (findScopeReferences)
* Target: src/features/repertoire/remove.ts (findScopeReferences)
*
* Scanner searches for @scope package references in:
* - {root}/pieces/**\/*.yaml
@ -18,10 +18,10 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { findScopeReferences } from '../features/ensemble/remove.js';
import { makeScanConfig } from './helpers/ensemble-test-helpers.js';
import { findScopeReferences } from '../features/repertoire/remove.js';
import { makeScanConfig } from './helpers/repertoire-test-helpers.js';
describe('ensemble reference integrity: detection', () => {
describe('repertoire reference integrity: detection', () => {
let tempDir: string;
beforeEach(() => {
@ -34,52 +34,52 @@ describe('ensemble reference integrity: detection', () => {
// U29: ~/.takt/pieces/ の @scope 参照を検出
// Given: {root}/pieces/my-review.yaml に
// persona: "@nrslib/takt-pack-fixture/expert-coder" を含む
// When: findScopeReferences("@nrslib/takt-pack-fixture", config)
// persona: "@nrslib/takt-ensemble-fixture/expert-coder" を含む
// When: findScopeReferences("@nrslib/takt-ensemble-fixture", config)
// Then: my-review.yaml が検出される
it('should detect @scope reference in global pieces YAML', () => {
const piecesDir = join(tempDir, 'pieces');
mkdirSync(piecesDir, { recursive: true });
const pieceFile = join(piecesDir, 'my-review.yaml');
writeFileSync(pieceFile, 'persona: "@nrslib/takt-pack-fixture/expert-coder"');
writeFileSync(pieceFile, 'persona: "@nrslib/takt-ensemble-fixture/expert-coder"');
const refs = findScopeReferences('@nrslib/takt-pack-fixture', makeScanConfig(tempDir));
const refs = findScopeReferences('@nrslib/takt-ensemble-fixture', makeScanConfig(tempDir));
expect(refs.some((r) => r.filePath === pieceFile)).toBe(true);
});
// U30: {root}/preferences/piece-categories.yaml の @scope 参照を検出
// Given: piece-categories.yaml に @nrslib/takt-pack-fixture/expert を含む
// When: findScopeReferences("@nrslib/takt-pack-fixture", config)
// Given: piece-categories.yaml に @nrslib/takt-ensemble-fixture/expert を含む
// When: findScopeReferences("@nrslib/takt-ensemble-fixture", config)
// Then: piece-categories.yaml が検出される
it('should detect @scope reference in global piece-categories.yaml', () => {
const prefsDir = join(tempDir, 'preferences');
mkdirSync(prefsDir, { recursive: true });
const categoriesFile = join(prefsDir, 'piece-categories.yaml');
writeFileSync(categoriesFile, 'categories:\n - "@nrslib/takt-pack-fixture/expert"');
writeFileSync(categoriesFile, 'categories:\n - "@nrslib/takt-ensemble-fixture/expert"');
const refs = findScopeReferences('@nrslib/takt-pack-fixture', makeScanConfig(tempDir));
const refs = findScopeReferences('@nrslib/takt-ensemble-fixture', makeScanConfig(tempDir));
expect(refs.some((r) => r.filePath === categoriesFile)).toBe(true);
});
// U31: {root}/.takt/pieces/ の @scope 参照を検出
// Given: プロジェクト {root}/.takt/pieces/proj.yaml に @scope 参照
// When: findScopeReferences("@nrslib/takt-pack-fixture", config)
// When: findScopeReferences("@nrslib/takt-ensemble-fixture", config)
// Then: proj.yaml が検出される
it('should detect @scope reference in project-level pieces YAML', () => {
const projectPiecesDir = join(tempDir, '.takt', 'pieces');
mkdirSync(projectPiecesDir, { recursive: true });
const projFile = join(projectPiecesDir, 'proj.yaml');
writeFileSync(projFile, 'persona: "@nrslib/takt-pack-fixture/expert-coder"');
writeFileSync(projFile, 'persona: "@nrslib/takt-ensemble-fixture/expert-coder"');
const refs = findScopeReferences('@nrslib/takt-pack-fixture', makeScanConfig(tempDir));
const refs = findScopeReferences('@nrslib/takt-ensemble-fixture', makeScanConfig(tempDir));
expect(refs.some((r) => r.filePath === projFile)).toBe(true);
});
});
describe('ensemble reference integrity: non-detection', () => {
describe('repertoire reference integrity: non-detection', () => {
let tempDir: string;
beforeEach(() => {
@ -92,28 +92,28 @@ describe('ensemble reference integrity: non-detection', () => {
// U32: @scope なし参照は検出しない
// Given: persona: "coder" のみ(@scope なし)
// When: findScopeReferences("@nrslib/takt-pack-fixture", config)
// When: findScopeReferences("@nrslib/takt-ensemble-fixture", config)
// Then: 結果が空配列
it('should not detect plain name references without @scope prefix', () => {
const piecesDir = join(tempDir, 'pieces');
mkdirSync(piecesDir, { recursive: true });
writeFileSync(join(piecesDir, 'plain.yaml'), 'persona: "coder"');
const refs = findScopeReferences('@nrslib/takt-pack-fixture', makeScanConfig(tempDir));
const refs = findScopeReferences('@nrslib/takt-ensemble-fixture', makeScanConfig(tempDir));
expect(refs).toHaveLength(0);
});
// U33: 別スコープは検出しない
// Given: persona: "@other/package/name"
// When: findScopeReferences("@nrslib/takt-pack-fixture", config)
// When: findScopeReferences("@nrslib/takt-ensemble-fixture", config)
// Then: 結果が空配列
it('should not detect references to a different @scope package', () => {
const piecesDir = join(tempDir, 'pieces');
mkdirSync(piecesDir, { recursive: true });
writeFileSync(join(piecesDir, 'other.yaml'), 'persona: "@other/package/name"');
const refs = findScopeReferences('@nrslib/takt-pack-fixture', makeScanConfig(tempDir));
const refs = findScopeReferences('@nrslib/takt-ensemble-fixture', makeScanConfig(tempDir));
expect(refs).toHaveLength(0);
});

View File

@ -1,5 +1,5 @@
/**
* Unit tests for ensemble @scope resolution and facet resolution chain.
* Unit tests for repertoire @scope resolution and facet resolution chain.
*
* Covers:
* A. @scope reference resolution (src/faceted-prompting/scope.ts)
@ -8,7 +8,7 @@
*
* @scope resolution rules:
* "@{owner}/{repo}/{name}" in a facet field
* {ensembleDir}/@{owner}/{repo}/facets/{type}/{name}.md
* {repertoireDir}/@{owner}/{repo}/facets/{type}/{name}.md
*
* Name constraints:
* owner: /^[a-z0-9][a-z0-9-]*$/ (lowercase only after normalization)
@ -16,7 +16,7 @@
* facet/piece name: /^[a-z0-9][a-z0-9-]*$/
*
* Facet resolution order (package piece):
* 1. package-local: {ensembleDir}/@{owner}/{repo}/facets/{type}/{facet}.md
* 1. package-local: {repertoireDir}/@{owner}/{repo}/facets/{type}/{facet}.md
* 2. project: .takt/facets/{type}/{facet}.md
* 3. user: ~/.takt/facets/{type}/{facet}.md
* 4. builtin: builtins/{lang}/facets/{type}/{facet}.md
@ -55,45 +55,45 @@ describe('@scope reference resolution', () => {
});
// U34: persona @scope 解決
// Input: "@nrslib/takt-pack-fixture/expert-coder" (personas field)
// Expect: resolves to {ensembleDir}/@nrslib/takt-pack-fixture/facets/personas/expert-coder.md
it('should resolve persona @scope reference to ensemble faceted path', () => {
const ensembleDir = tempDir;
const ref = '@nrslib/takt-pack-fixture/expert-coder';
// Input: "@nrslib/takt-ensemble-fixture/expert-coder" (personas field)
// Expect: resolves to {repertoireDir}/@nrslib/takt-ensemble-fixture/facets/personas/expert-coder.md
it('should resolve persona @scope reference to repertoire faceted path', () => {
const repertoireDir = tempDir;
const ref = '@nrslib/takt-ensemble-fixture/expert-coder';
const scopeRef = parseScopeRef(ref);
const resolved = resolveScopeRef(scopeRef, 'personas', ensembleDir);
const resolved = resolveScopeRef(scopeRef, 'personas', repertoireDir);
const expected = join(ensembleDir, '@nrslib', 'takt-pack-fixture', 'facets', 'personas', 'expert-coder.md');
const expected = join(repertoireDir, '@nrslib', 'takt-ensemble-fixture', 'facets', 'personas', 'expert-coder.md');
expect(resolved).toBe(expected);
});
// U35: policy @scope 解決
// Input: "@nrslib/takt-pack-fixture/strict-coding" (policies field)
// Expect: resolves to {ensembleDir}/@nrslib/takt-pack-fixture/facets/policies/strict-coding.md
it('should resolve policy @scope reference to ensemble faceted path', () => {
const ensembleDir = tempDir;
const ref = '@nrslib/takt-pack-fixture/strict-coding';
// Input: "@nrslib/takt-ensemble-fixture/strict-coding" (policies field)
// Expect: resolves to {repertoireDir}/@nrslib/takt-ensemble-fixture/facets/policies/strict-coding.md
it('should resolve policy @scope reference to repertoire faceted path', () => {
const repertoireDir = tempDir;
const ref = '@nrslib/takt-ensemble-fixture/strict-coding';
const scopeRef = parseScopeRef(ref);
const resolved = resolveScopeRef(scopeRef, 'policies', ensembleDir);
const resolved = resolveScopeRef(scopeRef, 'policies', repertoireDir);
const expected = join(ensembleDir, '@nrslib', 'takt-pack-fixture', 'facets', 'policies', 'strict-coding.md');
const expected = join(repertoireDir, '@nrslib', 'takt-ensemble-fixture', 'facets', 'policies', 'strict-coding.md');
expect(resolved).toBe(expected);
});
// U36: 大文字正規化
// Input: "@NrsLib/Takt-Pack-Fixture/expert-coder"
// Input: "@NrsLib/Takt-Ensemble-Fixture/expert-coder"
// Expect: owner and repo lowercase-normalized; name kept as-is (must already be lowercase per spec)
it('should normalize uppercase @scope references to lowercase before resolving', () => {
const ensembleDir = tempDir;
const ref = '@NrsLib/Takt-Pack-Fixture/expert-coder';
const repertoireDir = tempDir;
const ref = '@NrsLib/Takt-Ensemble-Fixture/expert-coder';
const scopeRef = parseScopeRef(ref);
// owner and repo are normalized to lowercase
expect(scopeRef.owner).toBe('nrslib');
expect(scopeRef.repo).toBe('takt-pack-fixture');
expect(scopeRef.repo).toBe('takt-ensemble-fixture');
const resolved = resolveScopeRef(scopeRef, 'personas', ensembleDir);
const expected = join(ensembleDir, '@nrslib', 'takt-pack-fixture', 'facets', 'personas', 'expert-coder.md');
const resolved = resolveScopeRef(scopeRef, 'personas', repertoireDir);
const expected = join(repertoireDir, '@nrslib', 'takt-ensemble-fixture', 'facets', 'personas', 'expert-coder.md');
expect(resolved).toBe(expected);
});
@ -101,13 +101,13 @@ describe('@scope reference resolution', () => {
// Input: "@nonexistent/package/facet"
// Expect: resolveFacetPath returns undefined (file not found at resolved path)
it('should throw error when @scope reference points to non-existent package', () => {
const ensembleDir = tempDir;
const repertoireDir = tempDir;
const ref = '@nonexistent/package/facet';
// resolveFacetPath returns undefined when the @scope file does not exist
const result = resolveFacetPath(ref, 'personas', {
lang: 'en',
ensembleDir,
repertoireDir,
});
expect(result).toBeUndefined();
@ -177,9 +177,9 @@ describe('facet resolution chain: package-local layer', () => {
// When: パッケージ内ピースからファセット解決
// Then: package-local 層のファセットが返る
it('should prefer package-local facet over project/user/builtin layers', () => {
const ensembleDir = join(tempDir, 'ensemble');
const packagePiecesDir = join(ensembleDir, '@nrslib', 'takt-pack-fixture', 'pieces');
const packageFacetDir = join(ensembleDir, '@nrslib', 'takt-pack-fixture', 'facets', 'personas');
const repertoireDir = join(tempDir, 'repertoire');
const packagePiecesDir = join(repertoireDir, '@nrslib', 'takt-ensemble-fixture', 'pieces');
const packageFacetDir = join(repertoireDir, '@nrslib', 'takt-ensemble-fixture', 'facets', 'personas');
const projectFacetDir = join(tempDir, 'project', '.takt', 'facets', 'personas');
// Create both package-local and project facet files with the same name
@ -192,7 +192,7 @@ describe('facet resolution chain: package-local layer', () => {
const candidateDirs = buildCandidateDirsWithPackage('personas', {
lang: 'en',
pieceDir: packagePiecesDir,
ensembleDir,
repertoireDir,
projectDir: join(tempDir, 'project'),
});
@ -205,8 +205,8 @@ describe('facet resolution chain: package-local layer', () => {
// When: ファセット解決
// Then: project 層のファセットが返る
it('should fall back to project facet when package-local does not have it', () => {
const ensembleDir = join(tempDir, 'ensemble');
const packagePiecesDir = join(ensembleDir, '@nrslib', 'takt-pack-fixture', 'pieces');
const repertoireDir = join(tempDir, 'repertoire');
const packagePiecesDir = join(repertoireDir, '@nrslib', 'takt-ensemble-fixture', 'pieces');
const projectFacetDir = join(tempDir, 'project', '.takt', 'facets', 'personas');
mkdirSync(packagePiecesDir, { recursive: true });
@ -218,7 +218,7 @@ describe('facet resolution chain: package-local layer', () => {
const resolved = resolveFacetPath('expert-coder', 'personas', {
lang: 'en',
pieceDir: packagePiecesDir,
ensembleDir,
repertoireDir,
projectDir: join(tempDir, 'project'),
});
@ -230,9 +230,9 @@ describe('facet resolution chain: package-local layer', () => {
// When: ファセット解決
// Then: package-local は無視。project → user → builtin の3層で解決
it('should not consult package-local layer for non-package pieces', () => {
const ensembleDir = join(tempDir, 'ensemble');
const packageFacetDir = join(ensembleDir, '@nrslib', 'takt-pack-fixture', 'facets', 'personas');
// Non-package pieceDir (not under ensembleDir)
const repertoireDir = join(tempDir, 'repertoire');
const packageFacetDir = join(repertoireDir, '@nrslib', 'takt-ensemble-fixture', 'facets', 'personas');
// Non-package pieceDir (not under repertoireDir)
const globalPiecesDir = join(tempDir, 'global-pieces');
mkdirSync(packageFacetDir, { recursive: true });
@ -242,7 +242,7 @@ describe('facet resolution chain: package-local layer', () => {
const candidateDirs = buildCandidateDirsWithPackage('personas', {
lang: 'en',
pieceDir: globalPiecesDir,
ensembleDir,
repertoireDir,
});
// Package-local dir should NOT be in candidates for non-package pieces
@ -252,14 +252,14 @@ describe('facet resolution chain: package-local layer', () => {
describe('package piece detection', () => {
// U46: パッケージ所属は pieceDir パスから判定
// Given: pieceDir が {ensembleDir}/@nrslib/repo/pieces/ 配下
// Given: pieceDir が {repertoireDir}/@nrslib/repo/pieces/ 配下
// When: isPackagePiece(pieceDir) 呼び出し
// Then: true が返る
it('should return true for pieceDir under ensemble/@scope/repo/pieces/', () => {
const ensembleDir = '/home/user/.takt/ensemble';
const pieceDir = '/home/user/.takt/ensemble/@nrslib/takt-pack-fixture/pieces';
it('should return true for pieceDir under repertoire/@scope/repo/pieces/', () => {
const repertoireDir = '/home/user/.takt/repertoire';
const pieceDir = '/home/user/.takt/repertoire/@nrslib/takt-ensemble-fixture/pieces';
expect(isPackagePiece(pieceDir, ensembleDir)).toBe(true);
expect(isPackagePiece(pieceDir, repertoireDir)).toBe(true);
});
// U47: 非パッケージ pieceDir は false
@ -267,9 +267,9 @@ describe('package piece detection', () => {
// When: isPackagePiece(pieceDir) 呼び出し
// Then: false が返る
it('should return false for pieceDir under global pieces directory', () => {
const ensembleDir = '/home/user/.takt/ensemble';
const repertoireDir = '/home/user/.takt/repertoire';
const pieceDir = '/home/user/.takt/pieces';
expect(isPackagePiece(pieceDir, ensembleDir)).toBe(false);
expect(isPackagePiece(pieceDir, repertoireDir)).toBe(false);
});
});

View File

@ -15,7 +15,7 @@ import {
cleanupResiduals,
atomicReplace,
type AtomicReplaceOptions,
} from '../../features/ensemble/atomic-update.js';
} from '../../features/repertoire/atomic-update.js';
// ---------------------------------------------------------------------------
// cleanupResiduals

View File

@ -27,7 +27,7 @@ import {
MAX_FILE_COUNT,
ALLOWED_EXTENSIONS,
ALLOWED_DIRS,
} from '../../features/ensemble/file-filter.js';
} from '../../features/repertoire/file-filter.js';
// ---------------------------------------------------------------------------
// isAllowedExtension
@ -39,7 +39,7 @@ describe('isAllowedExtension', () => {
});
it('should allow .yaml files', () => {
expect(isAllowedExtension('takt-package.yaml')).toBe(true);
expect(isAllowedExtension('takt-repertoire.yaml')).toBe(true);
});
it('should allow .yml files', () => {

View File

@ -9,7 +9,7 @@
*/
import { describe, it, expect, vi } from 'vitest';
import { resolveRef } from '../../features/ensemble/github-ref-resolver.js';
import { resolveRef } from '../../features/repertoire/github-ref-resolver.js';
describe('resolveRef', () => {
it('should return specRef directly when provided', () => {

View File

@ -5,7 +5,7 @@
*/
import { describe, it, expect } from 'vitest';
import { parseGithubSpec } from '../../features/ensemble/github-spec.js';
import { parseGithubSpec } from '../../features/repertoire/github-spec.js';
describe('parseGithubSpec', () => {
describe('happy path', () => {

View File

@ -1,10 +1,10 @@
/**
* Tests for ensemble list display data retrieval.
* Tests for repertoire list display data retrieval.
*
* Covers:
* - readPackageInfo(): reads description from takt-package.yaml and ref/commit from .takt-pack-lock.yaml
* - readPackageInfo(): reads description from takt-repertoire.yaml and ref/commit from .takt-repertoire-lock.yaml
* - commit is truncated to first 7 characters for display
* - listPackages(): enumerates all installed packages under ensemble/
* - listPackages(): enumerates all installed packages under repertoire/
* - Multiple packages are correctly listed
*/
@ -15,7 +15,7 @@ import { tmpdir } from 'node:os';
import {
readPackageInfo,
listPackages,
} from '../../features/ensemble/list.js';
} from '../../features/repertoire/list.js';
// ---------------------------------------------------------------------------
// readPackageInfo
@ -32,16 +32,16 @@ describe('readPackageInfo', () => {
rmSync(tempDir, { recursive: true, force: true });
});
it('should read description from takt-package.yaml', () => {
// Given: a package directory with takt-package.yaml and .takt-pack-lock.yaml
it('should read description from takt-repertoire.yaml', () => {
// Given: a package directory with takt-repertoire.yaml and .takt-repertoire-lock.yaml
const packageDir = join(tempDir, '@nrslib', 'takt-fullstack');
mkdirSync(packageDir, { recursive: true });
writeFileSync(
join(packageDir, 'takt-package.yaml'),
join(packageDir, 'takt-repertoire.yaml'),
'description: フルスタック開発ワークフロー\n',
);
writeFileSync(
join(packageDir, '.takt-pack-lock.yaml'),
join(packageDir, '.takt-repertoire-lock.yaml'),
`source: github:nrslib/takt-fullstack
ref: v1.2.0
commit: abc1234def5678
@ -63,9 +63,9 @@ imported_at: 2026-02-20T12:00:00.000Z
// Given: package with a long commit SHA
const packageDir = join(tempDir, '@nrslib', 'takt-security-facets');
mkdirSync(packageDir, { recursive: true });
writeFileSync(join(packageDir, 'takt-package.yaml'), 'description: Security facets\n');
writeFileSync(join(packageDir, 'takt-repertoire.yaml'), 'description: Security facets\n');
writeFileSync(
join(packageDir, '.takt-pack-lock.yaml'),
join(packageDir, '.takt-repertoire-lock.yaml'),
`source: github:nrslib/takt-security-facets
ref: HEAD
commit: def5678901234567
@ -82,12 +82,12 @@ imported_at: 2026-02-20T12:00:00.000Z
});
it('should handle package without description field', () => {
// Given: takt-package.yaml with no description
// Given: takt-repertoire.yaml with no description
const packageDir = join(tempDir, '@acme', 'takt-backend');
mkdirSync(packageDir, { recursive: true });
writeFileSync(join(packageDir, 'takt-package.yaml'), 'path: takt\n');
writeFileSync(join(packageDir, 'takt-repertoire.yaml'), 'path: takt\n');
writeFileSync(
join(packageDir, '.takt-pack-lock.yaml'),
join(packageDir, '.takt-repertoire-lock.yaml'),
`source: github:acme/takt-backend
ref: v2.0.0
commit: 789abcdef0123
@ -107,9 +107,9 @@ imported_at: 2026-01-15T08:30:00.000Z
// Given: package imported from default branch
const packageDir = join(tempDir, '@acme', 'no-tag-pkg');
mkdirSync(packageDir, { recursive: true });
writeFileSync(join(packageDir, 'takt-package.yaml'), 'description: No tag\n');
writeFileSync(join(packageDir, 'takt-repertoire.yaml'), 'description: No tag\n');
writeFileSync(
join(packageDir, '.takt-pack-lock.yaml'),
join(packageDir, '.takt-repertoire-lock.yaml'),
`source: github:acme/no-tag-pkg
ref: HEAD
commit: aabbccddeeff00
@ -128,8 +128,8 @@ imported_at: 2026-02-01T00:00:00.000Z
// Given: package directory with no lock file
const packageDir = join(tempDir, '@acme', 'no-lock-pkg');
mkdirSync(packageDir, { recursive: true });
writeFileSync(join(packageDir, 'takt-package.yaml'), 'description: No lock\n');
// .takt-pack-lock.yaml intentionally not created
writeFileSync(join(packageDir, 'takt-repertoire.yaml'), 'description: No lock\n');
// .takt-repertoire-lock.yaml intentionally not created
// When: package info is read
const info = readPackageInfo(packageDir, '@acme/no-lock-pkg');
@ -156,18 +156,18 @@ describe('listPackages', () => {
});
function createPackage(
ensembleDir: string,
repertoireDir: string,
owner: string,
repo: string,
description: string,
ref: string,
commit: string,
): void {
const packageDir = join(ensembleDir, `@${owner}`, repo);
const packageDir = join(repertoireDir, `@${owner}`, repo);
mkdirSync(packageDir, { recursive: true });
writeFileSync(join(packageDir, 'takt-package.yaml'), `description: ${description}\n`);
writeFileSync(join(packageDir, 'takt-repertoire.yaml'), `description: ${description}\n`);
writeFileSync(
join(packageDir, '.takt-pack-lock.yaml'),
join(packageDir, '.takt-repertoire-lock.yaml'),
`source: github:${owner}/${repo}
ref: ${ref}
commit: ${commit}
@ -176,15 +176,15 @@ imported_at: 2026-02-20T12:00:00.000Z
);
}
it('should list all installed packages from ensemble directory', () => {
// Given: ensemble directory with 3 packages
const ensembleDir = join(tempDir, 'ensemble');
createPackage(ensembleDir, 'nrslib', 'takt-fullstack', 'Fullstack workflow', 'v1.2.0', 'abc1234def5678');
createPackage(ensembleDir, 'nrslib', 'takt-security-facets', 'Security facets', 'HEAD', 'def5678901234');
createPackage(ensembleDir, 'acme-corp', 'takt-backend', 'Backend facets', 'v2.0.0', '789abcdef0123');
it('should list all installed packages from repertoire directory', () => {
// Given: repertoire directory with 3 packages
const repertoireDir = join(tempDir, 'repertoire');
createPackage(repertoireDir, 'nrslib', 'takt-fullstack', 'Fullstack workflow', 'v1.2.0', 'abc1234def5678');
createPackage(repertoireDir, 'nrslib', 'takt-security-facets', 'Security facets', 'HEAD', 'def5678901234');
createPackage(repertoireDir, 'acme-corp', 'takt-backend', 'Backend facets', 'v2.0.0', '789abcdef0123');
// When: packages are listed
const packages = listPackages(ensembleDir);
const packages = listPackages(repertoireDir);
// Then: all 3 packages are returned
expect(packages).toHaveLength(3);
@ -194,25 +194,25 @@ imported_at: 2026-02-20T12:00:00.000Z
expect(scopes).toContain('@acme-corp/takt-backend');
});
it('should return empty list when ensemble directory has no packages', () => {
// Given: empty ensemble directory
const ensembleDir = join(tempDir, 'ensemble');
mkdirSync(ensembleDir, { recursive: true });
it('should return empty list when repertoire directory has no packages', () => {
// Given: empty repertoire directory
const repertoireDir = join(tempDir, 'repertoire');
mkdirSync(repertoireDir, { recursive: true });
// When: packages are listed
const packages = listPackages(ensembleDir);
const packages = listPackages(repertoireDir);
// Then: empty list
expect(packages).toHaveLength(0);
});
it('should include correct commit (truncated to 7 chars) for each package', () => {
// Given: ensemble with one package
const ensembleDir = join(tempDir, 'ensemble');
createPackage(ensembleDir, 'nrslib', 'takt-fullstack', 'Fullstack', 'v1.2.0', 'abc1234def5678');
// Given: repertoire with one package
const repertoireDir = join(tempDir, 'repertoire');
createPackage(repertoireDir, 'nrslib', 'takt-fullstack', 'Fullstack', 'v1.2.0', 'abc1234def5678');
// When: packages are listed
const packages = listPackages(ensembleDir);
const packages = listPackages(repertoireDir);
// Then: commit is 7 chars
const pkg = packages.find((p) => p.scope === '@nrslib/takt-fullstack')!;

View File

@ -1,11 +1,11 @@
/**
* Tests for .takt-pack-lock.yaml generation and parsing.
* Tests for .takt-repertoire-lock.yaml generation and parsing.
*
* Covers:
* - extractCommitSha: parse SHA from tarball directory name {owner}-{repo}-{sha}/
* - generateLockFile: produces correct fields (source, ref, commit, imported_at)
* - ref defaults to "HEAD" when not specified
* - parseLockFile: reads .takt-pack-lock.yaml content
* - parseLockFile: reads .takt-repertoire-lock.yaml content
*/
import { describe, it, expect } from 'vitest';
@ -13,7 +13,7 @@ import {
extractCommitSha,
generateLockFile,
parseLockFile,
} from '../../features/ensemble/lock-file.js';
} from '../../features/repertoire/lock-file.js';
// ---------------------------------------------------------------------------
// extractCommitSha
@ -117,7 +117,7 @@ describe('generateLockFile', () => {
// ---------------------------------------------------------------------------
describe('parseLockFile', () => {
it('should parse a valid .takt-pack-lock.yaml string', () => {
it('should parse a valid .takt-repertoire-lock.yaml string', () => {
// Given: lock file YAML content
const yaml = `source: github:nrslib/takt-fullstack
ref: v1.2.0

View File

@ -8,7 +8,7 @@
*/
import { describe, it, expect } from 'vitest';
import { summarizeFacetsByType, detectEditPieces, formatEditPieceWarnings } from '../../features/ensemble/pack-summary.js';
import { summarizeFacetsByType, detectEditPieces, formatEditPieceWarnings } from '../../features/repertoire/pack-summary.js';
// ---------------------------------------------------------------------------
// summarizeFacetsByType

View File

@ -2,7 +2,7 @@
* Tests for package-local facet resolution chain.
*
* Covers:
* - isPackagePiece(): detects if pieceDir is under ~/.takt/ensemble/@owner/repo/pieces/
* - isPackagePiece(): detects if pieceDir is under ~/.takt/repertoire/@owner/repo/pieces/
* - getPackageFromPieceDir(): extracts @owner/repo from pieceDir path
* - Package pieces use 4-layer chain: package-local project user builtin
* - Non-package pieces use 3-layer chain: project user builtin
@ -34,27 +34,27 @@ describe('isPackagePiece', () => {
rmSync(tempDir, { recursive: true, force: true });
});
it('should return true when pieceDir is under ensemble/@owner/repo/pieces/', () => {
// Given: pieceDir under the ensemble directory structure
const ensembleDir = join(tempDir, 'ensemble');
const pieceDir = join(ensembleDir, '@nrslib', 'takt-fullstack', 'pieces');
it('should return true when pieceDir is under repertoire/@owner/repo/pieces/', () => {
// Given: pieceDir under the repertoire directory structure
const repertoireDir = join(tempDir, 'repertoire');
const pieceDir = join(repertoireDir, '@nrslib', 'takt-fullstack', 'pieces');
// When: checking if it is a package piece
const result = isPackagePiece(pieceDir, ensembleDir);
const result = isPackagePiece(pieceDir, repertoireDir);
// Then: it is recognized as a package piece
expect(result).toBe(true);
});
it('should return false when pieceDir is under user global pieces directory', () => {
// Given: pieceDir in ~/.takt/pieces/ (not ensemble)
// Given: pieceDir in ~/.takt/pieces/ (not repertoire)
const globalPiecesDir = join(tempDir, 'pieces');
mkdirSync(globalPiecesDir, { recursive: true });
const ensembleDir = join(tempDir, 'ensemble');
const repertoireDir = join(tempDir, 'repertoire');
// When: checking
const result = isPackagePiece(globalPiecesDir, ensembleDir);
const result = isPackagePiece(globalPiecesDir, repertoireDir);
// Then: not a package piece
expect(result).toBe(false);
@ -65,10 +65,10 @@ describe('isPackagePiece', () => {
const projectPiecesDir = join(tempDir, '.takt', 'pieces');
mkdirSync(projectPiecesDir, { recursive: true });
const ensembleDir = join(tempDir, 'ensemble');
const repertoireDir = join(tempDir, 'repertoire');
// When: checking
const result = isPackagePiece(projectPiecesDir, ensembleDir);
const result = isPackagePiece(projectPiecesDir, repertoireDir);
// Then: not a package piece
expect(result).toBe(false);
@ -79,10 +79,10 @@ describe('isPackagePiece', () => {
const builtinPiecesDir = join(tempDir, 'builtins', 'ja', 'pieces');
mkdirSync(builtinPiecesDir, { recursive: true });
const ensembleDir = join(tempDir, 'ensemble');
const repertoireDir = join(tempDir, 'repertoire');
// When: checking
const result = isPackagePiece(builtinPiecesDir, ensembleDir);
const result = isPackagePiece(builtinPiecesDir, repertoireDir);
// Then: not a package piece
expect(result).toBe(false);
@ -104,13 +104,13 @@ describe('getPackageFromPieceDir', () => {
rmSync(tempDir, { recursive: true, force: true });
});
it('should extract owner and repo from ensemble pieceDir', () => {
// Given: pieceDir under ensemble
const ensembleDir = join(tempDir, 'ensemble');
const pieceDir = join(ensembleDir, '@nrslib', 'takt-fullstack', 'pieces');
it('should extract owner and repo from repertoire pieceDir', () => {
// Given: pieceDir under repertoire
const repertoireDir = join(tempDir, 'repertoire');
const pieceDir = join(repertoireDir, '@nrslib', 'takt-fullstack', 'pieces');
// When: package is extracted
const pkg = getPackageFromPieceDir(pieceDir, ensembleDir);
const pkg = getPackageFromPieceDir(pieceDir, repertoireDir);
// Then: owner and repo are correct
expect(pkg).not.toBeUndefined();
@ -119,12 +119,12 @@ describe('getPackageFromPieceDir', () => {
});
it('should return undefined for non-package pieceDir', () => {
// Given: pieceDir not under ensemble
// Given: pieceDir not under repertoire
const pieceDir = join(tempDir, 'pieces');
const ensembleDir = join(tempDir, 'ensemble');
const repertoireDir = join(tempDir, 'repertoire');
// When: package is extracted
const pkg = getPackageFromPieceDir(pieceDir, ensembleDir);
const pkg = getPackageFromPieceDir(pieceDir, repertoireDir);
// Then: undefined (not a package piece)
expect(pkg).toBeUndefined();
@ -148,25 +148,25 @@ describe('buildCandidateDirsWithPackage', () => {
it('should include package-local dir as first candidate for package piece', () => {
// Given: a package piece context
const ensembleDir = join(tempDir, 'ensemble');
const pieceDir = join(ensembleDir, '@nrslib', 'takt-fullstack', 'pieces');
const repertoireDir = join(tempDir, 'repertoire');
const pieceDir = join(repertoireDir, '@nrslib', 'takt-fullstack', 'pieces');
const projectDir = join(tempDir, 'project');
const context = { projectDir, lang: 'ja' as const, pieceDir, ensembleDir };
const context = { projectDir, lang: 'ja' as const, pieceDir, repertoireDir };
// When: candidate directories are built
const dirs = buildCandidateDirsWithPackage('personas', context);
// Then: package-local dir is first
const expectedPackageLocal = join(ensembleDir, '@nrslib', 'takt-fullstack', 'facets', 'personas');
const expectedPackageLocal = join(repertoireDir, '@nrslib', 'takt-fullstack', 'facets', 'personas');
expect(dirs[0]).toBe(expectedPackageLocal);
});
it('should have 4 candidate dirs for package piece: package-local, project, user, builtin', () => {
// Given: package piece context
const ensembleDir = join(tempDir, 'ensemble');
const pieceDir = join(ensembleDir, '@nrslib', 'takt-fullstack', 'pieces');
const repertoireDir = join(tempDir, 'repertoire');
const pieceDir = join(repertoireDir, '@nrslib', 'takt-fullstack', 'pieces');
const projectDir = join(tempDir, 'project');
const context = { projectDir, lang: 'ja' as const, pieceDir, ensembleDir };
const context = { projectDir, lang: 'ja' as const, pieceDir, repertoireDir };
// When: candidate directories are built
const dirs = buildCandidateDirsWithPackage('personas', context);
@ -176,14 +176,14 @@ describe('buildCandidateDirsWithPackage', () => {
});
it('should have 3 candidate dirs for non-package piece: project, user, builtin', () => {
// Given: non-package piece context (no ensemble path)
// Given: non-package piece context (no repertoire path)
const projectDir = join(tempDir, 'project');
const userPiecesDir = join(tempDir, 'pieces');
const context = {
projectDir,
lang: 'ja' as const,
pieceDir: userPiecesDir,
ensembleDir: join(tempDir, 'ensemble'),
repertoireDir: join(tempDir, 'repertoire'),
};
// When: candidate directories are built
@ -195,8 +195,8 @@ describe('buildCandidateDirsWithPackage', () => {
it('should resolve package-local facet before project-level for package piece', () => {
// Given: both package-local and project-level facet files exist
const ensembleDir = join(tempDir, 'ensemble');
const pkgFacetDir = join(ensembleDir, '@nrslib', 'takt-fullstack', 'facets', 'personas');
const repertoireDir = join(tempDir, 'repertoire');
const pkgFacetDir = join(repertoireDir, '@nrslib', 'takt-fullstack', 'facets', 'personas');
mkdirSync(pkgFacetDir, { recursive: true });
writeFileSync(join(pkgFacetDir, 'expert-coder.md'), 'Package persona');
@ -205,8 +205,8 @@ describe('buildCandidateDirsWithPackage', () => {
mkdirSync(projectFacetDir, { recursive: true });
writeFileSync(join(projectFacetDir, 'expert-coder.md'), 'Project persona');
const pieceDir = join(ensembleDir, '@nrslib', 'takt-fullstack', 'pieces');
const context = { projectDir, lang: 'ja' as const, pieceDir, ensembleDir };
const pieceDir = join(repertoireDir, '@nrslib', 'takt-fullstack', 'pieces');
const context = { projectDir, lang: 'ja' as const, pieceDir, repertoireDir };
// When: candidate directories are built
const dirs = buildCandidateDirsWithPackage('personas', context);

View File

@ -1,5 +1,5 @@
/**
* Tests for reference integrity check during ensemble remove.
* Tests for reference integrity check during repertoire remove.
*
* Covers:
* - shouldRemoveOwnerDir(): returns true when owner dir has no other packages
@ -9,7 +9,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { shouldRemoveOwnerDir } from '../../features/ensemble/remove.js';
import { shouldRemoveOwnerDir } from '../../features/repertoire/remove.js';
// ---------------------------------------------------------------------------
// shouldRemoveOwnerDir

View File

@ -1,5 +1,5 @@
/**
* Regression test for ensembleRemoveCommand scan configuration.
* Regression test for repertoireRemoveCommand scan configuration.
*
* Verifies that findScopeReferences is called with exactly the 3 spec-defined
* scan locations:
@ -20,14 +20,14 @@ vi.mock('node:fs', () => ({
rmSync: vi.fn(),
}));
vi.mock('../../features/ensemble/remove.js', () => ({
vi.mock('../../features/repertoire/remove.js', () => ({
findScopeReferences: vi.fn().mockReturnValue([]),
shouldRemoveOwnerDir: vi.fn().mockReturnValue(false),
}));
vi.mock('../../infra/config/paths.js', () => ({
getEnsembleDir: vi.fn().mockReturnValue('/home/user/.takt/ensemble'),
getEnsemblePackageDir: vi.fn().mockReturnValue('/home/user/.takt/ensemble/@owner/repo'),
getRepertoireDir: vi.fn().mockReturnValue('/home/user/.takt/repertoire'),
getRepertoirePackageDir: vi.fn().mockReturnValue('/home/user/.takt/repertoire/@owner/repo'),
getGlobalConfigDir: vi.fn().mockReturnValue('/home/user/.takt'),
getGlobalPiecesDir: vi.fn().mockReturnValue('/home/user/.takt/pieces'),
getProjectPiecesDir: vi.fn().mockReturnValue('/project/.takt/pieces'),
@ -46,21 +46,21 @@ vi.mock('../../shared/ui/index.js', () => ({
// Import after mocks are declared
// ---------------------------------------------------------------------------
import { ensembleRemoveCommand } from '../../commands/ensemble/remove.js';
import { findScopeReferences } from '../../features/ensemble/remove.js';
import { repertoireRemoveCommand } from '../../commands/repertoire/remove.js';
import { findScopeReferences } from '../../features/repertoire/remove.js';
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('ensembleRemoveCommand — scan configuration', () => {
describe('repertoireRemoveCommand — scan configuration', () => {
beforeEach(() => {
vi.mocked(findScopeReferences).mockReturnValue([]);
});
it('should call findScopeReferences with exactly 2 piecesDirs and 1 categoriesFile', async () => {
// When: remove command is invoked (confirm returns false → no deletion)
await ensembleRemoveCommand('@owner/repo');
await repertoireRemoveCommand('@owner/repo');
// Then: findScopeReferences is called once
expect(findScopeReferences).toHaveBeenCalledOnce();
@ -76,7 +76,7 @@ describe('ensembleRemoveCommand — scan configuration', () => {
it('should include global pieces dir in scan', async () => {
// When: remove command is invoked
await ensembleRemoveCommand('@owner/repo');
await repertoireRemoveCommand('@owner/repo');
const [, scanConfig] = vi.mocked(findScopeReferences).mock.calls[0]!;
@ -86,7 +86,7 @@ describe('ensembleRemoveCommand — scan configuration', () => {
it('should include project pieces dir in scan', async () => {
// When: remove command is invoked
await ensembleRemoveCommand('@owner/repo');
await repertoireRemoveCommand('@owner/repo');
const [, scanConfig] = vi.mocked(findScopeReferences).mock.calls[0]!;
@ -96,7 +96,7 @@ describe('ensembleRemoveCommand — scan configuration', () => {
it('should include preferences/piece-categories.yaml in categoriesFiles', async () => {
// When: remove command is invoked
await ensembleRemoveCommand('@owner/repo');
await repertoireRemoveCommand('@owner/repo');
const [, scanConfig] = vi.mocked(findScopeReferences).mock.calls[0]!;
@ -108,7 +108,7 @@ describe('ensembleRemoveCommand — scan configuration', () => {
it('should pass the scope as the first argument to findScopeReferences', async () => {
// When: remove command is invoked with a scope
await ensembleRemoveCommand('@owner/repo');
await repertoireRemoveCommand('@owner/repo');
const [scope] = vi.mocked(findScopeReferences).mock.calls[0]!;

View File

@ -2,7 +2,7 @@
* Tests for facet directory path helpers in paths.ts items 4245.
*
* Verifies the `facets/` segment is present in all facet path results,
* and that getEnsembleFacetDir constructs the correct full ensemble path.
* and that getRepertoireFacetDir constructs the correct full repertoire path.
*/
import { describe, it, expect } from 'vitest';
@ -10,8 +10,8 @@ import {
getProjectFacetDir,
getGlobalFacetDir,
getBuiltinFacetDir,
getEnsembleFacetDir,
getEnsemblePackageDir,
getRepertoireFacetDir,
getRepertoirePackageDir,
type FacetType,
} from '../../infra/config/paths.js';
@ -130,38 +130,38 @@ describe('getBuiltinFacetDir — facets/ prefix', () => {
});
// ---------------------------------------------------------------------------
// getEnsembleFacetDir — item 45 (new function)
// getRepertoireFacetDir — item 45 (new function)
// ---------------------------------------------------------------------------
describe('getEnsembleFacetDir — new path function', () => {
it('should return path containing ensemble/@{owner}/{repo}/facets/{type}', () => {
describe('getRepertoireFacetDir — new path function', () => {
it('should return path containing repertoire/@{owner}/{repo}/facets/{type}', () => {
// Given: owner, repo, and facet type
// When: path is built
const dir = getEnsembleFacetDir('nrslib', 'takt-fullstack', 'personas');
const dir = getRepertoireFacetDir('nrslib', 'takt-fullstack', 'personas');
// Then: all segments are present
const normalized = dir.replace(/\\/g, '/');
expect(normalized).toContain('ensemble');
expect(normalized).toContain('repertoire');
expect(normalized).toContain('@nrslib');
expect(normalized).toContain('takt-fullstack');
expect(normalized).toContain('facets');
expect(normalized).toContain('personas');
});
it('should construct path as ~/.takt/ensemble/@{owner}/{repo}/facets/{type}', () => {
it('should construct path as ~/.takt/repertoire/@{owner}/{repo}/facets/{type}', () => {
// Given: owner, repo, and facet type
// When: path is built
const dir = getEnsembleFacetDir('nrslib', 'takt-fullstack', 'personas');
const dir = getRepertoireFacetDir('nrslib', 'takt-fullstack', 'personas');
// Then: full segment order is ensemble → @nrslib → takt-fullstack → facets → personas
// Then: full segment order is repertoire → @nrslib → takt-fullstack → facets → personas
const normalized = dir.replace(/\\/g, '/');
expect(normalized).toMatch(/ensemble\/@nrslib\/takt-fullstack\/facets\/personas/);
expect(normalized).toMatch(/repertoire\/@nrslib\/takt-fullstack\/facets\/personas/);
});
it('should prepend @ before owner name in the path', () => {
// Given: owner without @ prefix
// When: path is built
const dir = getEnsembleFacetDir('myowner', 'myrepo', 'policies');
const dir = getRepertoireFacetDir('myowner', 'myrepo', 'policies');
// Then: @ is included before owner in the path
const normalized = dir.replace(/\\/g, '/');
@ -172,46 +172,46 @@ describe('getEnsembleFacetDir — new path function', () => {
// Given: all valid facet types
for (const t of ALL_FACET_TYPES) {
// When: path is built
const dir = getEnsembleFacetDir('owner', 'repo', t);
const dir = getRepertoireFacetDir('owner', 'repo', t);
// Then: path has correct ensemble structure with facet type
// Then: path has correct repertoire structure with facet type
const normalized = dir.replace(/\\/g, '/');
expect(normalized).toMatch(new RegExp(`ensemble/@owner/repo/facets/${t}`));
expect(normalized).toMatch(new RegExp(`repertoire/@owner/repo/facets/${t}`));
}
});
});
// ---------------------------------------------------------------------------
// getEnsemblePackageDir — item 46
// getRepertoirePackageDir — item 46
// ---------------------------------------------------------------------------
describe('getEnsemblePackageDir', () => {
it('should return path containing ensemble/@{owner}/{repo}', () => {
describe('getRepertoirePackageDir', () => {
it('should return path containing repertoire/@{owner}/{repo}', () => {
// Given: owner and repo
// When: path is built
const dir = getEnsemblePackageDir('nrslib', 'takt-fullstack');
const dir = getRepertoirePackageDir('nrslib', 'takt-fullstack');
// Then: all segments are present
const normalized = dir.replace(/\\/g, '/');
expect(normalized).toContain('ensemble');
expect(normalized).toContain('repertoire');
expect(normalized).toContain('@nrslib');
expect(normalized).toContain('takt-fullstack');
});
it('should construct path as ~/.takt/ensemble/@{owner}/{repo}', () => {
it('should construct path as ~/.takt/repertoire/@{owner}/{repo}', () => {
// Given: owner and repo
// When: path is built
const dir = getEnsemblePackageDir('nrslib', 'takt-fullstack');
const dir = getRepertoirePackageDir('nrslib', 'takt-fullstack');
// Then: full segment order is ensemble → @nrslib → takt-fullstack
// Then: full segment order is repertoire → @nrslib → takt-fullstack
const normalized = dir.replace(/\\/g, '/');
expect(normalized).toMatch(/ensemble\/@nrslib\/takt-fullstack$/);
expect(normalized).toMatch(/repertoire\/@nrslib\/takt-fullstack$/);
});
it('should prepend @ before owner name in the path', () => {
// Given: owner without @ prefix
// When: path is built
const dir = getEnsemblePackageDir('myowner', 'myrepo');
const dir = getRepertoirePackageDir('myowner', 'myrepo');
// Then: @ is included before owner in the path
const normalized = dir.replace(/\\/g, '/');

View File

@ -1,5 +1,5 @@
/**
* Tests for takt-package.yaml parsing and validation.
* Tests for takt-repertoire.yaml parsing and validation.
*
* Covers:
* - Full field parsing (description, path, takt.min_version)
@ -14,23 +14,23 @@ import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import {
parseTaktPackConfig,
validateTaktPackPath,
parseTaktRepertoireConfig,
validateTaktRepertoirePath,
validateMinVersion,
isVersionCompatible,
checkPackageHasContent,
checkPackageHasContentWithContext,
validateRealpathInsideRoot,
resolvePackConfigPath,
} from '../../features/ensemble/takt-pack-config.js';
resolveRepertoireConfigPath,
} from '../../features/repertoire/takt-repertoire-config.js';
// ---------------------------------------------------------------------------
// parseTaktPackConfig
// parseTaktRepertoireConfig
// ---------------------------------------------------------------------------
describe('parseTaktPackConfig', () => {
describe('parseTaktRepertoireConfig', () => {
it('should parse all fields when present', () => {
// Given: a complete takt-package.yaml content
// Given: a complete takt-repertoire.yaml content
const yaml = `
description: My package
path: takt
@ -39,7 +39,7 @@ takt:
`.trim();
// When: parsed
const config = parseTaktPackConfig(yaml);
const config = parseTaktRepertoireConfig(yaml);
// Then: all fields are populated
expect(config.description).toBe('My package');
@ -48,11 +48,11 @@ takt:
});
it('should default path to "." when omitted', () => {
// Given: takt-package.yaml with no path field
// Given: takt-repertoire.yaml with no path field
const yaml = `description: No path field`;
// When: parsed
const config = parseTaktPackConfig(yaml);
const config = parseTaktRepertoireConfig(yaml);
// Then: path defaults to "."
expect(config.path).toBe('.');
@ -63,7 +63,7 @@ takt:
const yaml = '';
// When: parsed
const config = parseTaktPackConfig(yaml);
const config = parseTaktRepertoireConfig(yaml);
// Then: defaults are applied
expect(config.path).toBe('.');
@ -76,7 +76,7 @@ takt:
const yaml = 'description: セキュリティレビュー用ファセット集';
// When: parsed
const config = parseTaktPackConfig(yaml);
const config = parseTaktRepertoireConfig(yaml);
// Then: description is set, path defaults to "."
expect(config.description).toBe('セキュリティレビュー用ファセット集');
@ -88,7 +88,7 @@ takt:
const yaml = 'path: pkg/takt';
// When: parsed
const config = parseTaktPackConfig(yaml);
const config = parseTaktRepertoireConfig(yaml);
// Then: path is preserved as-is
expect(config.path).toBe('pkg/takt');
@ -96,49 +96,49 @@ takt:
});
// ---------------------------------------------------------------------------
// validateTaktPackPath
// validateTaktRepertoirePath
// ---------------------------------------------------------------------------
describe('validateTaktPackPath', () => {
describe('validateTaktRepertoirePath', () => {
it('should accept "." (current directory)', () => {
// Given: default path
// When: validated
// Then: no error thrown
expect(() => validateTaktPackPath('.')).not.toThrow();
expect(() => validateTaktRepertoirePath('.')).not.toThrow();
});
it('should accept simple relative path "takt"', () => {
expect(() => validateTaktPackPath('takt')).not.toThrow();
expect(() => validateTaktRepertoirePath('takt')).not.toThrow();
});
it('should accept nested relative path "pkg/takt"', () => {
expect(() => validateTaktPackPath('pkg/takt')).not.toThrow();
expect(() => validateTaktRepertoirePath('pkg/takt')).not.toThrow();
});
it('should reject absolute path starting with "/"', () => {
// Given: absolute path
// When: validated
// Then: throws an error
expect(() => validateTaktPackPath('/etc/passwd')).toThrow();
expect(() => validateTaktRepertoirePath('/etc/passwd')).toThrow();
});
it('should reject path starting with "~"', () => {
// Given: home-relative path
expect(() => validateTaktPackPath('~/takt')).toThrow();
expect(() => validateTaktRepertoirePath('~/takt')).toThrow();
});
it('should reject path containing ".." segment', () => {
// Given: path with directory traversal
expect(() => validateTaktPackPath('../outside')).toThrow();
expect(() => validateTaktRepertoirePath('../outside')).toThrow();
});
it('should reject path with ".." in middle segment', () => {
// Given: path with ".." embedded
expect(() => validateTaktPackPath('takt/../etc')).toThrow();
expect(() => validateTaktRepertoirePath('takt/../etc')).toThrow();
});
it('should reject "../../etc" (multiple traversal)', () => {
expect(() => validateTaktPackPath('../../etc')).toThrow();
expect(() => validateTaktRepertoirePath('../../etc')).toThrow();
});
});
@ -234,7 +234,7 @@ describe('checkPackageHasContent', () => {
let tempDir: string;
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'takt-pack-content-'));
tempDir = mkdtempSync(join(tmpdir(), 'takt-repertoire-content-'));
});
afterEach(() => {
@ -249,7 +249,7 @@ describe('checkPackageHasContent', () => {
});
it('should include manifest/path/hint details in contextual error', () => {
const manifestPath = join(tempDir, '.takt', 'takt-package.yaml');
const manifestPath = join(tempDir, '.takt', 'takt-repertoire.yaml');
expect(() => checkPackageHasContentWithContext(tempDir, {
manifestPath,
configuredPath: '.',
@ -344,10 +344,10 @@ describe('validateRealpathInsideRoot', () => {
});
// ---------------------------------------------------------------------------
// resolvePackConfigPath (takt-package.yaml search order)
// resolveRepertoireConfigPath (takt-repertoire.yaml search order)
// ---------------------------------------------------------------------------
describe('resolvePackConfigPath', () => {
describe('resolveRepertoireConfigPath', () => {
let extractDir: string;
beforeEach(() => {
@ -358,47 +358,47 @@ describe('resolvePackConfigPath', () => {
rmSync(extractDir, { recursive: true, force: true });
});
it('should return .takt/takt-package.yaml when only that path exists', () => {
// Given: only .takt/takt-package.yaml exists
it('should return .takt/takt-repertoire.yaml when only that path exists', () => {
// Given: only .takt/takt-repertoire.yaml exists
const taktDir = join(extractDir, '.takt');
mkdirSync(taktDir, { recursive: true });
writeFileSync(join(taktDir, 'takt-package.yaml'), 'description: dot-takt');
writeFileSync(join(taktDir, 'takt-repertoire.yaml'), 'description: dot-takt');
// When: resolved
const result = resolvePackConfigPath(extractDir);
const result = resolveRepertoireConfigPath(extractDir);
// Then: .takt/takt-package.yaml is returned
expect(result).toBe(join(extractDir, '.takt', 'takt-package.yaml'));
// Then: .takt/takt-repertoire.yaml is returned
expect(result).toBe(join(extractDir, '.takt', 'takt-repertoire.yaml'));
});
it('should return root takt-package.yaml when only that path exists', () => {
// Given: only root takt-package.yaml exists
writeFileSync(join(extractDir, 'takt-package.yaml'), 'description: root');
it('should return root takt-repertoire.yaml when only that path exists', () => {
// Given: only root takt-repertoire.yaml exists
writeFileSync(join(extractDir, 'takt-repertoire.yaml'), 'description: root');
// When: resolved
const result = resolvePackConfigPath(extractDir);
const result = resolveRepertoireConfigPath(extractDir);
// Then: root takt-package.yaml is returned
expect(result).toBe(join(extractDir, 'takt-package.yaml'));
// Then: root takt-repertoire.yaml is returned
expect(result).toBe(join(extractDir, 'takt-repertoire.yaml'));
});
it('should prefer .takt/takt-package.yaml when both paths exist', () => {
// Given: both .takt/takt-package.yaml and root takt-package.yaml exist
it('should prefer .takt/takt-repertoire.yaml when both paths exist', () => {
// Given: both .takt/takt-repertoire.yaml and root takt-repertoire.yaml exist
const taktDir = join(extractDir, '.takt');
mkdirSync(taktDir, { recursive: true });
writeFileSync(join(taktDir, 'takt-package.yaml'), 'description: dot-takt');
writeFileSync(join(extractDir, 'takt-package.yaml'), 'description: root');
writeFileSync(join(taktDir, 'takt-repertoire.yaml'), 'description: dot-takt');
writeFileSync(join(extractDir, 'takt-repertoire.yaml'), 'description: root');
// When: resolved
const result = resolvePackConfigPath(extractDir);
const result = resolveRepertoireConfigPath(extractDir);
// Then: .takt/takt-package.yaml takes precedence
expect(result).toBe(join(extractDir, '.takt', 'takt-package.yaml'));
// Then: .takt/takt-repertoire.yaml takes precedence
expect(result).toBe(join(extractDir, '.takt', 'takt-repertoire.yaml'));
});
it('should throw when neither path exists', () => {
// Given: empty extract directory
// When / Then: throws an error
expect(() => resolvePackConfigPath(extractDir)).toThrow('takt-package.yaml not found in');
expect(() => resolveRepertoireConfigPath(extractDir)).toThrow('takt-repertoire.yaml not found in');
});
});

View File

@ -11,7 +11,7 @@
*/
import { describe, it, expect } from 'vitest';
import { parseTarVerboseListing } from '../../features/ensemble/tar-parser.js';
import { parseTarVerboseListing } from '../../features/repertoire/tar-parser.js';
// ---------------------------------------------------------------------------
// Helpers to build realistic tar verbose lines

View File

@ -183,12 +183,12 @@ describe('resolveAutoPr default in selectAndExecuteTask', () => {
expect(mockSelectPiece).toHaveBeenCalledWith('/project');
});
it('should accept ensemble scoped piece override when it exists', async () => {
mockLoadPieceByIdentifier.mockReturnValueOnce({ name: '@nrslib/takt-packages/critical-thinking' } as never);
it('should accept repertoire scoped piece override when it exists', async () => {
mockLoadPieceByIdentifier.mockReturnValueOnce({ name: '@nrslib/takt-ensembles/critical-thinking' } as never);
const selected = await determinePiece('/project', '@nrslib/takt-packages/critical-thinking');
const selected = await determinePiece('/project', '@nrslib/takt-ensembles/critical-thinking');
expect(selected).toBe('@nrslib/takt-packages/critical-thinking');
expect(selected).toBe('@nrslib/takt-ensembles/critical-thinking');
});
it('should fail task record when executeTask throws', async () => {

View File

@ -1,7 +1,7 @@
/**
* Unit tests for takt-package.yaml schema validation.
* Unit tests for takt-repertoire.yaml schema validation.
*
* Target: src/features/ensemble/takt-pack-config.ts
* Target: src/features/repertoire/takt-repertoire-config.ts
*
* Schema rules under test:
* - description: optional
@ -13,46 +13,46 @@
import { describe, it, expect } from 'vitest';
import {
parseTaktPackConfig,
validateTaktPackPath,
parseTaktRepertoireConfig,
validateTaktRepertoirePath,
validateMinVersion,
} from '../features/ensemble/takt-pack-config.js';
} from '../features/repertoire/takt-repertoire-config.js';
describe('takt-package.yaml schema: description field', () => {
describe('takt-repertoire.yaml schema: description field', () => {
it('should accept schema without description field', () => {
const config = parseTaktPackConfig('');
const config = parseTaktRepertoireConfig('');
expect(config.description).toBeUndefined();
});
});
describe('takt-package.yaml schema: path field', () => {
describe('takt-repertoire.yaml schema: path field', () => {
it('should default path to "." when not specified', () => {
const config = parseTaktPackConfig('');
const config = parseTaktRepertoireConfig('');
expect(config.path).toBe('.');
});
it('should reject path starting with "/" (absolute path)', () => {
expect(() => validateTaktPackPath('/foo')).toThrow();
expect(() => validateTaktRepertoirePath('/foo')).toThrow();
});
it('should reject path starting with "~" (tilde-absolute path)', () => {
expect(() => validateTaktPackPath('~/foo')).toThrow();
expect(() => validateTaktRepertoirePath('~/foo')).toThrow();
});
it('should reject path with ".." segment traversing outside repository', () => {
expect(() => validateTaktPackPath('../outside')).toThrow();
expect(() => validateTaktRepertoirePath('../outside')).toThrow();
});
it('should reject path with embedded ".." segments leading outside repository', () => {
expect(() => validateTaktPackPath('sub/../../../outside')).toThrow();
expect(() => validateTaktRepertoirePath('sub/../../../outside')).toThrow();
});
it('should accept valid relative path "sub/dir"', () => {
expect(() => validateTaktPackPath('sub/dir')).not.toThrow();
expect(() => validateTaktRepertoirePath('sub/dir')).not.toThrow();
});
});
describe('takt-package.yaml schema: takt.min_version field', () => {
describe('takt-repertoire.yaml schema: takt.min_version field', () => {
it('should accept min_version "0.5.0" (valid semver)', () => {
expect(() => validateMinVersion('0.5.0')).not.toThrow();
});

View File

@ -15,9 +15,9 @@ import { showCatalog } from '../../features/catalog/index.js';
import { computeReviewMetrics, formatReviewMetrics, parseSinceDuration, purgeOldEvents } from '../../features/analytics/index.js';
import { program, resolvedCwd } from './program.js';
import { resolveAgentOverrides } from './helpers.js';
import { ensembleAddCommand } from '../../commands/ensemble/add.js';
import { ensembleRemoveCommand } from '../../commands/ensemble/remove.js';
import { ensembleListCommand } from '../../commands/ensemble/list.js';
import { repertoireAddCommand } from '../../commands/repertoire/add.js';
import { repertoireRemoveCommand } from '../../commands/repertoire/remove.js';
import { repertoireListCommand } from '../../commands/repertoire/list.js';
program
.command('run')
@ -177,29 +177,29 @@ program
}
});
const ensemble = program
.command('ensemble')
.description('Manage ensemble packages');
const repertoire = program
.command('repertoire')
.description('Manage repertoire packages');
ensemble
repertoire
.command('add')
.description('Install an ensemble package from GitHub')
.description('Install a repertoire package from GitHub')
.argument('<spec>', 'Package spec (e.g. github:{owner}/{repo}@{ref})')
.action(async (spec: string) => {
await ensembleAddCommand(spec);
await repertoireAddCommand(spec);
});
ensemble
repertoire
.command('remove')
.description('Remove an installed ensemble package')
.description('Remove an installed repertoire package')
.argument('<scope>', 'Package scope (e.g. @{owner}/{repo})')
.action(async (scope: string) => {
await ensembleRemoveCommand(scope);
await repertoireRemoveCommand(scope);
});
ensemble
repertoire
.command('list')
.description('List installed ensemble packages')
.description('List installed repertoire packages')
.action(async () => {
await ensembleListCommand();
await repertoireListCommand();
});

View File

@ -1,9 +1,9 @@
/**
* takt ensemble add install an ensemble package from GitHub.
* takt repertoire add install a repertoire package from GitHub.
*
* Usage:
* takt ensemble add github:{owner}/{repo}@{ref}
* takt ensemble add github:{owner}/{repo} (uses default branch)
* takt repertoire add github:{owner}/{repo}@{ref}
* takt repertoire add github:{owner}/{repo} (uses default branch)
*/
import { mkdirSync, copyFileSync, existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
@ -12,24 +12,24 @@ import { tmpdir } from 'node:os';
import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { stringify as stringifyYaml } from 'yaml';
import { getEnsemblePackageDir } from '../../infra/config/paths.js';
import { parseGithubSpec } from '../../features/ensemble/github-spec.js';
import { getRepertoirePackageDir } from '../../infra/config/paths.js';
import { parseGithubSpec } from '../../features/repertoire/github-spec.js';
import {
parseTaktPackConfig,
validateTaktPackPath,
parseTaktRepertoireConfig,
validateTaktRepertoirePath,
validateMinVersion,
isVersionCompatible,
checkPackageHasContentWithContext,
validateRealpathInsideRoot,
resolvePackConfigPath,
} from '../../features/ensemble/takt-pack-config.js';
import { collectCopyTargets } from '../../features/ensemble/file-filter.js';
import { parseTarVerboseListing } from '../../features/ensemble/tar-parser.js';
import { resolveRef } from '../../features/ensemble/github-ref-resolver.js';
import { atomicReplace, cleanupResiduals } from '../../features/ensemble/atomic-update.js';
import { generateLockFile, extractCommitSha } from '../../features/ensemble/lock-file.js';
import { TAKT_PACKAGE_MANIFEST_FILENAME } from '../../features/ensemble/constants.js';
import { summarizeFacetsByType, detectEditPieces, formatEditPieceWarnings } from '../../features/ensemble/pack-summary.js';
resolveRepertoireConfigPath,
} from '../../features/repertoire/takt-repertoire-config.js';
import { collectCopyTargets } from '../../features/repertoire/file-filter.js';
import { parseTarVerboseListing } from '../../features/repertoire/tar-parser.js';
import { resolveRef } from '../../features/repertoire/github-ref-resolver.js';
import { atomicReplace, cleanupResiduals } from '../../features/repertoire/atomic-update.js';
import { generateLockFile, extractCommitSha } from '../../features/repertoire/lock-file.js';
import { TAKT_REPERTOIRE_MANIFEST_FILENAME, TAKT_REPERTOIRE_LOCK_FILENAME } from '../../features/repertoire/constants.js';
import { summarizeFacetsByType, detectEditPieces, formatEditPieceWarnings } from '../../features/repertoire/pack-summary.js';
import { confirm } from '../../shared/prompt/index.js';
import { info, success } from '../../shared/ui/index.js';
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
@ -37,9 +37,9 @@ import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
const require = createRequire(import.meta.url);
const { version: TAKT_VERSION } = require('../../../package.json') as { version: string };
const log = createLogger('ensemble-add');
const log = createLogger('repertoire-add');
export async function ensembleAddCommand(spec: string): Promise<void> {
export async function repertoireAddCommand(spec: string): Promise<void> {
const { owner, repo, ref: specRef } = parseGithubSpec(spec);
try {
@ -93,11 +93,11 @@ export async function ensembleAddCommand(spec: string): Promise<void> {
);
}
const packConfigPath = resolvePackConfigPath(tmpExtractDir);
const packConfigPath = resolveRepertoireConfigPath(tmpExtractDir);
const packConfigYaml = readFileSync(packConfigPath, 'utf-8');
const config = parseTaktPackConfig(packConfigYaml);
validateTaktPackPath(config.path);
const config = parseTaktRepertoireConfig(packConfigYaml);
validateTaktRepertoirePath(config.path);
if (config.takt?.min_version) {
validateMinVersion(config.takt.min_version);
@ -157,7 +157,7 @@ export async function ensembleAddCommand(spec: string): Promise<void> {
return;
}
const packageDir = getEnsemblePackageDir(owner, repo);
const packageDir = getRepertoirePackageDir(owner, repo);
if (existsSync(packageDir)) {
const overwrite = await confirm(
@ -180,7 +180,7 @@ export async function ensembleAddCommand(spec: string): Promise<void> {
mkdirSync(dirname(destFile), { recursive: true });
copyFileSync(target.absolutePath, destFile);
}
copyFileSync(packConfigPath, join(packageDir, TAKT_PACKAGE_MANIFEST_FILENAME));
copyFileSync(packConfigPath, join(packageDir, TAKT_REPERTOIRE_MANIFEST_FILENAME));
const lock = generateLockFile({
source: `github:${owner}/${repo}`,
@ -188,7 +188,7 @@ export async function ensembleAddCommand(spec: string): Promise<void> {
commitSha,
importedAt: new Date(),
});
writeFileSync(join(packageDir, '.takt-pack-lock.yaml'), stringifyYaml(lock));
writeFileSync(join(packageDir, TAKT_REPERTOIRE_LOCK_FILENAME), stringifyYaml(lock));
},
});

View File

@ -1,13 +1,13 @@
/**
* takt ensemble list list installed ensemble packages.
* takt repertoire list list installed repertoire packages.
*/
import { getEnsembleDir } from '../../infra/config/paths.js';
import { listPackages } from '../../features/ensemble/list.js';
import { getRepertoireDir } from '../../infra/config/paths.js';
import { listPackages } from '../../features/repertoire/list.js';
import { info } from '../../shared/ui/index.js';
export async function ensembleListCommand(): Promise<void> {
const packages = listPackages(getEnsembleDir());
export async function repertoireListCommand(): Promise<void> {
const packages = listPackages(getRepertoireDir());
if (packages.length === 0) {
info('インストール済みパッケージはありません');

View File

@ -1,15 +1,15 @@
/**
* takt ensemble remove remove an installed ensemble package.
* takt repertoire remove remove an installed repertoire package.
*/
import { rmSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { getEnsembleDir, getEnsemblePackageDir, getGlobalConfigDir, getGlobalPiecesDir, getProjectPiecesDir } from '../../infra/config/paths.js';
import { findScopeReferences, shouldRemoveOwnerDir } from '../../features/ensemble/remove.js';
import { getRepertoireDir, getRepertoirePackageDir, getGlobalConfigDir, getGlobalPiecesDir, getProjectPiecesDir } from '../../infra/config/paths.js';
import { findScopeReferences, shouldRemoveOwnerDir } from '../../features/repertoire/remove.js';
import { confirm } from '../../shared/prompt/index.js';
import { info, success } from '../../shared/ui/index.js';
export async function ensembleRemoveCommand(scope: string): Promise<void> {
export async function repertoireRemoveCommand(scope: string): Promise<void> {
if (!scope.startsWith('@')) {
throw new Error(`Invalid scope: "${scope}". Expected @{owner}/{repo}`);
}
@ -21,8 +21,8 @@ export async function ensembleRemoveCommand(scope: string): Promise<void> {
const owner = withoutAt.slice(0, slashIdx);
const repo = withoutAt.slice(slashIdx + 1);
const ensembleDir = getEnsembleDir();
const packageDir = getEnsemblePackageDir(owner, repo);
const repertoireDir = getRepertoireDir();
const packageDir = getRepertoirePackageDir(owner, repo);
if (!existsSync(packageDir)) {
throw new Error(`Package not found: ${scope}`);
@ -47,7 +47,7 @@ export async function ensembleRemoveCommand(scope: string): Promise<void> {
rmSync(packageDir, { recursive: true, force: true });
const ownerDir = join(ensembleDir, `@${owner}`);
const ownerDir = join(repertoireDir, `@${owner}`);
if (shouldRemoveOwnerDir(ownerDir, repo)) {
rmSync(ownerDir, { recursive: true, force: true });
}

View File

@ -1,10 +1,10 @@
/**
* @scope reference resolution utilities for TAKT ensemble packages.
* @scope reference resolution utilities for TAKT repertoire packages.
*
* Provides:
* - isScopeRef(): detect @{owner}/{repo}/{facet-name} format
* - parseScopeRef(): parse and normalize components
* - resolveScopeRef(): build file path in ensemble directory
* - resolveScopeRef(): build file path in repertoire directory
* - validateScopeOwner/Repo/FacetName(): name constraint validation
*/
@ -51,22 +51,22 @@ export function parseScopeRef(ref: string): ScopeRef {
}
/**
* Resolve a scope reference to a file path in the ensemble directory.
* Resolve a scope reference to a file path in the repertoire directory.
*
* Path: {ensembleDir}/@{owner}/{repo}/facets/{facetType}/{name}.md
* Path: {repertoireDir}/@{owner}/{repo}/facets/{facetType}/{name}.md
*
* @param scopeRef - parsed scope reference
* @param facetType - e.g. "personas", "policies", "knowledge"
* @param ensembleDir - root ensemble directory (e.g. ~/.takt/ensemble)
* @param repertoireDir - root repertoire directory (e.g. ~/.takt/repertoire)
* @returns Absolute path to the facet file.
*/
export function resolveScopeRef(
scopeRef: ScopeRef,
facetType: string,
ensembleDir: string,
repertoireDir: string,
): string {
return join(
ensembleDir,
repertoireDir,
`@${scopeRef.owner}`,
scopeRef.repo,
'facets',

View File

@ -1,6 +0,0 @@
/**
* Shared constants for ensemble package manifest handling.
*/
/** Manifest filename inside a package repository and installed package directory. */
export const TAKT_PACKAGE_MANIFEST_FILENAME = 'takt-package.yaml';

View File

@ -0,0 +1,12 @@
/**
* Shared constants for repertoire package manifest handling.
*/
/** Directory name for the repertoire packages dir (~/.takt/repertoire). */
export const REPERTOIRE_DIR_NAME = 'repertoire';
/** Manifest filename inside a package repository and installed package directory. */
export const TAKT_REPERTOIRE_MANIFEST_FILENAME = 'takt-repertoire.yaml';
/** Lock file filename inside an installed package directory. */
export const TAKT_REPERTOIRE_LOCK_FILENAME = '.takt-repertoire-lock.yaml';

View File

@ -1,5 +1,5 @@
/**
* File filtering for ensemble package copy operations.
* File filtering for repertoire package copy operations.
*
* Security constraints:
* - Only .md, .yaml, .yml files are copied
@ -13,9 +13,9 @@ import { lstatSync, readdirSync, type Stats } from 'node:fs';
import { join, extname, relative } from 'node:path';
import { createLogger } from '../../shared/utils/debug.js';
const log = createLogger('ensemble-file-filter');
const log = createLogger('repertoire-file-filter');
/** Allowed file extensions for ensemble package files. */
/** Allowed file extensions for repertoire package files. */
export const ALLOWED_EXTENSIONS = ['.md', '.yaml', '.yml'] as const;
/** Top-level directories that are copied from a package. */
@ -107,7 +107,7 @@ function collectFromDir(
* Symbolic links are skipped. Files over MAX_FILE_SIZE are skipped.
* Throws if total file count exceeds MAX_FILE_COUNT.
*
* @param packageRoot - absolute path to the package root (respects takt-package.yaml path)
* @param packageRoot - absolute path to the package root (respects takt-repertoire.yaml path)
*/
export function collectCopyTargets(packageRoot: string): CopyTarget[] {
const targets: CopyTarget[] = [];

View File

@ -1,5 +1,5 @@
/**
* GitHub ref resolver for ensemble add command.
* GitHub ref resolver for repertoire add command.
*
* Resolves the ref for a GitHub package installation.
* When the spec omits @{ref}, queries the GitHub API for the default branch.

View File

@ -1,5 +1,5 @@
/**
* GitHub package spec parser for ensemble add command.
* GitHub package spec parser for repertoire add command.
*
* Parses "github:{owner}/{repo}@{ref}" format into structured components.
* The @{ref} part is optional; when omitted, ref is undefined and the caller

View File

@ -1,18 +1,18 @@
/**
* Ensemble package listing.
* Repertoire package listing.
*
* Scans the ensemble directory for installed packages and reads their
* Scans the repertoire directory for installed packages and reads their
* metadata (description, ref, truncated commit SHA) for display.
*/
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { parseTaktPackConfig } from './takt-pack-config.js';
import { parseTaktRepertoireConfig } from './takt-repertoire-config.js';
import { parseLockFile } from './lock-file.js';
import { TAKT_PACKAGE_MANIFEST_FILENAME } from './constants.js';
import { TAKT_REPERTOIRE_MANIFEST_FILENAME, TAKT_REPERTOIRE_LOCK_FILENAME } from './constants.js';
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
const log = createLogger('ensemble-list');
const log = createLogger('repertoire-list');
export interface PackageInfo {
/** e.g. "@nrslib/takt-fullstack" */
@ -30,13 +30,13 @@ export interface PackageInfo {
* @param scope - e.g. "@nrslib/takt-fullstack"
*/
export function readPackageInfo(packageDir: string, scope: string): PackageInfo {
const packConfigPath = join(packageDir, TAKT_PACKAGE_MANIFEST_FILENAME);
const lockPath = join(packageDir, '.takt-pack-lock.yaml');
const packConfigPath = join(packageDir, TAKT_REPERTOIRE_MANIFEST_FILENAME);
const lockPath = join(packageDir, TAKT_REPERTOIRE_LOCK_FILENAME);
const configYaml = existsSync(packConfigPath)
? readFileSync(packConfigPath, 'utf-8')
: '';
const config = parseTaktPackConfig(configYaml);
const config = parseTaktRepertoireConfig(configYaml);
const lockYaml = existsSync(lockPath)
? readFileSync(lockPath, 'utf-8')
@ -52,25 +52,25 @@ export function readPackageInfo(packageDir: string, scope: string): PackageInfo
}
/**
* List all installed packages under the ensemble directory.
* List all installed packages under the repertoire directory.
*
* Directory structure:
* ensembleDir/
* repertoireDir/
* @{owner}/
* {repo}/
* takt-package.yaml
* .takt-pack-lock.yaml
* takt-repertoire.yaml
* .takt-repertoire-lock.yaml
*
* @param ensembleDir - absolute path to the ensemble root (~/.takt/ensemble)
* @param repertoireDir - absolute path to the repertoire root (~/.takt/repertoire)
*/
export function listPackages(ensembleDir: string): PackageInfo[] {
if (!existsSync(ensembleDir)) return [];
export function listPackages(repertoireDir: string): PackageInfo[] {
if (!existsSync(repertoireDir)) return [];
const packages: PackageInfo[] = [];
for (const ownerEntry of readdirSync(ensembleDir)) {
for (const ownerEntry of readdirSync(repertoireDir)) {
if (!ownerEntry.startsWith('@')) continue;
const ownerDir = join(ensembleDir, ownerEntry);
const ownerDir = join(repertoireDir, ownerEntry);
try { if (!statSync(ownerDir).isDirectory()) continue; } catch (e) { log.debug(`stat failed for ${ownerDir}: ${getErrorMessage(e)}`); continue; }
for (const repoEntry of readdirSync(ownerDir)) {

View File

@ -1,7 +1,7 @@
/**
* Lock file generation and parsing for ensemble packages.
* Lock file generation and parsing for repertoire packages.
*
* The .takt-pack-lock.yaml records the installation provenance:
* The .takt-repertoire-lock.yaml records the installation provenance:
* source: github:{owner}/{repo}
* ref: tag or branch (defaults to "HEAD")
* commit: full SHA from tarball directory name
@ -59,7 +59,7 @@ export function generateLockFile(params: GenerateLockFileParams): PackageLock {
}
/**
* Parse .takt-pack-lock.yaml content into a PackageLock object.
* Parse .takt-repertoire-lock.yaml content into a PackageLock object.
* Returns empty-valued lock when yaml is empty (lock file missing).
*/
export function parseLockFile(yaml: string): PackageLock {

View File

@ -1,5 +1,5 @@
/**
* Ensemble package removal helpers.
* Repertoire package removal helpers.
*
* Provides:
* - findScopeReferences: scan YAML files for @scope references (for pre-removal warning)
@ -10,7 +10,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { createLogger } from '../../shared/utils/debug.js';
const log = createLogger('ensemble-remove');
const log = createLogger('repertoire-remove');
export interface ScopeReference {
/** Absolute path to the file containing the @scope reference. */

View File

@ -1,5 +1,5 @@
/**
* takt-package.yaml parsing and validation.
* takt-repertoire.yaml parsing and validation.
*
* Handles:
* - YAML parsing with default values
@ -13,9 +13,9 @@
import { existsSync, realpathSync } from 'node:fs';
import { join } from 'node:path';
import { parse as parseYaml } from 'yaml';
import { TAKT_PACKAGE_MANIFEST_FILENAME } from './constants.js';
import { TAKT_REPERTOIRE_MANIFEST_FILENAME } from './constants.js';
export interface TaktPackConfig {
export interface TaktRepertoireConfig {
description?: string;
path: string;
takt?: {
@ -31,10 +31,10 @@ interface PackageContentCheckContext {
const SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
/**
* Parse takt-package.yaml content string into a TaktPackConfig.
* Parse takt-repertoire.yaml content string into a TaktRepertoireConfig.
* Applies default path "." when not specified.
*/
export function parseTaktPackConfig(yaml: string): TaktPackConfig {
export function parseTaktRepertoireConfig(yaml: string): TaktRepertoireConfig {
const raw = (yaml.trim() ? parseYaml(yaml) : {}) as Record<string, unknown> | null;
const data = raw ?? {};
@ -56,16 +56,16 @@ export function parseTaktPackConfig(yaml: string): TaktPackConfig {
*
* Throws on validation failure.
*/
export function validateTaktPackPath(path: string): void {
export function validateTaktRepertoirePath(path: string): void {
if (path.startsWith('/')) {
throw new Error(`${TAKT_PACKAGE_MANIFEST_FILENAME}: path must not be absolute, got "${path}"`);
throw new Error(`${TAKT_REPERTOIRE_MANIFEST_FILENAME}: path must not be absolute, got "${path}"`);
}
if (path.startsWith('~')) {
throw new Error(`${TAKT_PACKAGE_MANIFEST_FILENAME}: path must not start with "~", got "${path}"`);
throw new Error(`${TAKT_REPERTOIRE_MANIFEST_FILENAME}: path must not start with "~", got "${path}"`);
}
const segments = path.split('/');
if (segments.includes('..')) {
throw new Error(`${TAKT_PACKAGE_MANIFEST_FILENAME}: path must not contain ".." segments, got "${path}"`);
throw new Error(`${TAKT_REPERTOIRE_MANIFEST_FILENAME}: path must not contain ".." segments, got "${path}"`);
}
}
@ -78,7 +78,7 @@ export function validateTaktPackPath(path: string): void {
export function validateMinVersion(version: string): void {
if (!SEMVER_PATTERN.test(version)) {
throw new Error(
`${TAKT_PACKAGE_MANIFEST_FILENAME}: takt.min_version must match X.Y.Z (no "v" prefix, no pre-release), got "${version}"`,
`${TAKT_REPERTOIRE_MANIFEST_FILENAME}: takt.min_version must match X.Y.Z (no "v" prefix, no pre-release), got "${version}"`,
);
}
}
@ -137,7 +137,7 @@ export function checkPackageHasContentWithContext(
const configuredPath = context.configuredPath ?? '.';
const manifestPath = context.manifestPath ?? '(unknown)';
const hint = configuredPath === '.'
? `hint: If your package content is under ".takt/", set "path: .takt" in ${TAKT_PACKAGE_MANIFEST_FILENAME}.`
? `hint: If your package content is under ".takt/", set "path: .takt" in ${TAKT_REPERTOIRE_MANIFEST_FILENAME}.`
: `hint: Verify "path: ${configuredPath}" points to a directory containing facets/ or pieces/.`;
throw new Error(
@ -154,24 +154,24 @@ export function checkPackageHasContentWithContext(
}
/**
* Resolve the path to takt-package.yaml within an extracted tarball directory.
* Resolve the path to takt-repertoire.yaml within an extracted tarball directory.
*
* Search order (first found wins):
* 1. {extractDir}/.takt/takt-package.yaml
* 2. {extractDir}/takt-package.yaml
* 1. {extractDir}/.takt/takt-repertoire.yaml
* 2. {extractDir}/takt-repertoire.yaml
*
* @param extractDir - root of the extracted tarball
* @throws if neither candidate exists
*/
export function resolvePackConfigPath(extractDir: string): string {
const taktDirPath = join(extractDir, '.takt', TAKT_PACKAGE_MANIFEST_FILENAME);
export function resolveRepertoireConfigPath(extractDir: string): string {
const taktDirPath = join(extractDir, '.takt', TAKT_REPERTOIRE_MANIFEST_FILENAME);
if (existsSync(taktDirPath)) return taktDirPath;
const rootPath = join(extractDir, TAKT_PACKAGE_MANIFEST_FILENAME);
const rootPath = join(extractDir, TAKT_REPERTOIRE_MANIFEST_FILENAME);
if (existsSync(rootPath)) return rootPath;
throw new Error(
`${TAKT_PACKAGE_MANIFEST_FILENAME} not found in "${extractDir}": checked .takt/${TAKT_PACKAGE_MANIFEST_FILENAME} and ${TAKT_PACKAGE_MANIFEST_FILENAME}`,
`${TAKT_REPERTOIRE_MANIFEST_FILENAME} not found in "${extractDir}": checked .takt/${TAKT_REPERTOIRE_MANIFEST_FILENAME} and ${TAKT_REPERTOIRE_MANIFEST_FILENAME}`,
);
}

View File

@ -16,7 +16,7 @@ import {
getBuiltinPiecesDir,
getGlobalFacetDir,
getProjectFacetDir,
getEnsembleDir,
getRepertoireDir,
isPathSafe,
} from '../paths.js';
import { resolveConfigValue } from '../resolveConfigValue.js';
@ -31,7 +31,7 @@ function getAllowedPromptBases(cwd: string): string[] {
getBuiltinPiecesDir(lang),
getGlobalFacetDir('personas'),
getProjectFacetDir(cwd, 'personas'),
getEnsembleDir(),
getRepertoireDir(),
];
}

View File

@ -326,11 +326,11 @@ function buildCategoryTree(
}
/**
* Append an "ensemble" category containing all @scope pieces.
* Append a "repertoire" category containing all @scope pieces.
* Creates one subcategory per @owner/repo package.
* Marks ensemble piece names as categorized (prevents them from appearing in "Others").
* Marks repertoire piece names as categorized (prevents them from appearing in "Others").
*/
function appendEnsembleCategory(
function appendRepertoireCategory(
categories: PieceCategoryNode[],
allPieces: Map<string, PieceWithSource>,
categorized: Set<string>,
@ -352,11 +352,11 @@ function appendEnsembleCategory(
categorized.add(pieceName);
}
if (packagePieces.size === 0) return categories;
const ensembleChildren: PieceCategoryNode[] = [];
const repertoireChildren: PieceCategoryNode[] = [];
for (const [packageKey, pieces] of packagePieces.entries()) {
ensembleChildren.push({ name: packageKey, pieces, children: [] });
repertoireChildren.push({ name: packageKey, pieces, children: [] });
}
return [...categories, { name: 'ensemble', pieces: [], children: ensembleChildren }];
return [...categories, { name: 'repertoire', pieces: [], children: repertoireChildren }];
}
function appendOthersCategory(
@ -415,7 +415,7 @@ export function buildCategorizedPieces(
const categorized = new Set<string>();
const categories = buildCategoryTree(config.pieceCategories, allPieces, categorized);
const categoriesWithEnsemble = appendEnsembleCategory(categories, allPieces, categorized);
const categoriesWithEnsemble = appendRepertoireCategory(categories, allPieces, categorized);
const finalCategories = config.showOthersCategory
? appendOthersCategory(categoriesWithEnsemble, allPieces, categorized, config.othersCategoryName)

View File

@ -12,7 +12,7 @@ import type { z } from 'zod';
import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js';
import type { PieceConfig, PieceMovement, PieceRule, OutputContractEntry, OutputContractItem, LoopMonitorConfig, LoopMonitorJudge, ArpeggioMovementConfig, ArpeggioMergeMovementConfig, TeamLeaderConfig } from '../../../core/models/index.js';
import { resolvePieceConfigValue } from '../resolvePieceConfigValue.js';
import { getEnsembleDir } from '../paths.js';
import { getRepertoireDir } from '../paths.js';
import {
type PieceSections,
type FacetResolutionContext,
@ -443,7 +443,7 @@ export function loadPieceFromFile(filePath: string, projectDir: string): PieceCo
lang: resolvePieceConfigValue(projectDir, 'language'),
projectDir,
pieceDir,
ensembleDir: getEnsembleDir(),
repertoireDir: getRepertoireDir(),
};
return normalizePieceConfig(raw, pieceDir, context);

View File

@ -9,7 +9,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { join, resolve, isAbsolute } from 'node:path';
import { homedir } from 'node:os';
import type { PieceConfig, PieceMovement, InteractiveMode } from '../../../core/models/index.js';
import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir, getEnsembleDir } from '../paths.js';
import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir, getRepertoireDir } from '../paths.js';
import { isScopeRef, parseScopeRef } from '../../../faceted-prompting/index.js';
import { resolvePieceConfigValues } from '../resolvePieceConfigValue.js';
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
@ -17,7 +17,7 @@ import { loadPieceFromFile } from './pieceParser.js';
const log = createLogger('piece-resolver');
export type PieceSource = 'builtin' | 'user' | 'project' | 'ensemble';
export type PieceSource = 'builtin' | 'user' | 'project' | 'repertoire';
export interface PieceWithSource {
config: PieceConfig;
@ -144,7 +144,7 @@ export function loadPieceByIdentifier(
projectCwd: string,
): PieceConfig | null {
if (isScopeRef(identifier)) {
return loadEnsemblePieceByRef(identifier, projectCwd);
return loadRepertoirePieceByRef(identifier, projectCwd);
}
if (isPiecePath(identifier)) {
return loadPieceFromPath(identifier, projectCwd, projectCwd);
@ -376,14 +376,14 @@ function* iteratePieceDir(
}
/**
* Iterate piece YAML files in all ensemble packages.
* Iterate piece YAML files in all repertoire packages.
* Qualified name format: @{owner}/{repo}/{piece-name}
*/
function* iterateEnsemblePieces(ensembleDir: string): Generator<PieceDirEntry> {
if (!existsSync(ensembleDir)) return;
for (const ownerEntry of readdirSync(ensembleDir)) {
function* iterateRepertoirePieces(repertoireDir: string): Generator<PieceDirEntry> {
if (!existsSync(repertoireDir)) return;
for (const ownerEntry of readdirSync(repertoireDir)) {
if (!ownerEntry.startsWith('@')) continue;
const ownerPath = join(ensembleDir, ownerEntry);
const ownerPath = join(repertoireDir, ownerEntry);
try { if (!statSync(ownerPath).isDirectory()) continue; } catch (e) { log.debug(`stat failed for owner dir ${ownerPath}: ${getErrorMessage(e)}`); continue; }
const owner = ownerEntry.slice(1);
for (const repoEntry of readdirSync(ownerPath)) {
@ -396,7 +396,7 @@ function* iterateEnsemblePieces(ensembleDir: string): Generator<PieceDirEntry> {
const piecePath = join(piecesDir, pieceFile);
try { if (!statSync(piecePath).isFile()) continue; } catch (e) { log.debug(`stat failed for piece file ${piecePath}: ${getErrorMessage(e)}`); continue; }
const pieceName = pieceFile.replace(/\.ya?ml$/, '');
yield { name: `@${owner}/${repoEntry}/${pieceName}`, path: piecePath, source: 'ensemble' };
yield { name: `@${owner}/${repoEntry}/${pieceName}`, path: piecePath, source: 'repertoire' };
}
}
}
@ -404,12 +404,12 @@ function* iterateEnsemblePieces(ensembleDir: string): Generator<PieceDirEntry> {
/**
* Load a piece by @scope reference (@{owner}/{repo}/{piece-name}).
* Resolves to ~/.takt/ensemble/@{owner}/{repo}/pieces/{piece-name}.yaml
* Resolves to ~/.takt/repertoire/@{owner}/{repo}/pieces/{piece-name}.yaml
*/
function loadEnsemblePieceByRef(identifier: string, projectCwd: string): PieceConfig | null {
function loadRepertoirePieceByRef(identifier: string, projectCwd: string): PieceConfig | null {
const scopeRef = parseScopeRef(identifier);
const ensembleDir = getEnsembleDir();
const piecesDir = join(ensembleDir, `@${scopeRef.owner}`, scopeRef.repo, 'pieces');
const repertoireDir = getRepertoireDir();
const piecesDir = join(repertoireDir, `@${scopeRef.owner}`, scopeRef.repo, 'pieces');
const filePath = resolvePieceFile(piecesDir, scopeRef.name);
if (!filePath) return null;
return loadPieceFromFile(filePath, projectCwd);
@ -450,12 +450,12 @@ export function loadAllPiecesWithSources(cwd: string): Map<string, PieceWithSour
}
}
const ensembleDir = getEnsembleDir();
for (const entry of iterateEnsemblePieces(ensembleDir)) {
const repertoireDir = getRepertoireDir();
for (const entry of iterateRepertoirePieces(repertoireDir)) {
try {
pieces.set(entry.name, { config: loadPieceFromFile(entry.path, cwd), source: entry.source });
} catch (err) {
log.debug('Skipping invalid ensemble piece file', { path: entry.path, error: getErrorMessage(err) });
log.debug('Skipping invalid repertoire piece file', { path: entry.path, error: getErrorMessage(err) });
}
}

View File

@ -11,7 +11,7 @@ import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import type { Language } from '../../../core/models/index.js';
import type { FacetType } from '../paths.js';
import { getProjectFacetDir, getGlobalFacetDir, getBuiltinFacetDir, getEnsembleFacetDir } from '../paths.js';
import { getProjectFacetDir, getGlobalFacetDir, getBuiltinFacetDir, getRepertoireFacetDir } from '../paths.js';
import {
resolveFacetPath as resolveFacetPathGeneric,
@ -38,39 +38,39 @@ export interface FacetResolutionContext {
lang: Language;
/** pieceDir of the piece being parsed — used for package-local layer detection. */
pieceDir?: string;
/** ensemble directory root — used together with pieceDir to detect package pieces. */
ensembleDir?: string;
/** repertoire directory root — used together with pieceDir to detect package pieces. */
repertoireDir?: string;
}
/**
* Determine whether a piece is inside an ensemble package.
* Determine whether a piece is inside a repertoire package.
*
* @param pieceDir - absolute path to the piece directory
* @param ensembleDir - absolute path to the ensemble root (~/.takt/ensemble)
* @param repertoireDir - absolute path to the repertoire root (~/.takt/repertoire)
*/
export function isPackagePiece(pieceDir: string, ensembleDir: string): boolean {
export function isPackagePiece(pieceDir: string, repertoireDir: string): boolean {
const resolvedPiece = resolve(pieceDir);
const resolvedEnsemble = resolve(ensembleDir);
return resolvedPiece.startsWith(resolvedEnsemble + '/');
const resolvedRepertoire = resolve(repertoireDir);
return resolvedPiece.startsWith(resolvedRepertoire + '/');
}
/**
* Extract { owner, repo } from a package piece directory path.
*
* Directory structure: {ensembleDir}/@{owner}/{repo}/pieces/
* Directory structure: {repertoireDir}/@{owner}/{repo}/pieces/
*
* @returns { owner, repo } if pieceDir is a package piece, undefined otherwise.
*/
export function getPackageFromPieceDir(
pieceDir: string,
ensembleDir: string,
repertoireDir: string,
): { owner: string; repo: string } | undefined {
if (!isPackagePiece(pieceDir, ensembleDir)) {
if (!isPackagePiece(pieceDir, repertoireDir)) {
return undefined;
}
const resolvedEnsemble = resolve(ensembleDir);
const resolvedRepertoire = resolve(repertoireDir);
const resolvedPiece = resolve(pieceDir);
const relative = resolvedPiece.slice(resolvedEnsemble.length + 1);
const relative = resolvedPiece.slice(resolvedRepertoire.length + 1);
const parts = relative.split('/');
if (parts.length < 2) return undefined;
const ownerWithAt = parts[0]!;
@ -84,7 +84,7 @@ export function getPackageFromPieceDir(
* Build candidate directories with optional package-local layer (4-layer for package pieces).
*
* Resolution order for package pieces:
* 1. package-local: {ensembleDir}/@{owner}/{repo}/facets/{type}
* 1. package-local: {repertoireDir}/@{owner}/{repo}/facets/{type}
* 2. project: {projectDir}/.takt/facets/{type}
* 3. user: ~/.takt/facets/{type}
* 4. builtin: builtins/{lang}/facets/{type}
@ -97,10 +97,10 @@ export function buildCandidateDirsWithPackage(
): string[] {
const dirs: string[] = [];
if (context.pieceDir && context.ensembleDir) {
const pkg = getPackageFromPieceDir(context.pieceDir, context.ensembleDir);
if (context.pieceDir && context.repertoireDir) {
const pkg = getPackageFromPieceDir(context.pieceDir, context.repertoireDir);
if (pkg) {
dirs.push(getEnsembleFacetDir(pkg.owner, pkg.repo, facetType, context.ensembleDir));
dirs.push(getRepertoireFacetDir(pkg.owner, pkg.repo, facetType, context.repertoireDir));
}
}
@ -116,7 +116,7 @@ export function buildCandidateDirsWithPackage(
/**
* Resolve a facet name to its file path via 4-layer lookup (package-local project user builtin).
*
* Handles @{owner}/{repo}/{facet-name} scope references directly when ensembleDir is provided.
* Handles @{owner}/{repo}/{facet-name} scope references directly when repertoireDir is provided.
*
* @returns Absolute file path if found, undefined otherwise.
*/
@ -125,9 +125,9 @@ export function resolveFacetPath(
facetType: FacetType,
context: FacetResolutionContext,
): string | undefined {
if (isScopeRef(name) && context.ensembleDir) {
if (isScopeRef(name) && context.repertoireDir) {
const scopeRef = parseScopeRef(name);
const filePath = resolveScopeRef(scopeRef, facetType, context.ensembleDir);
const filePath = resolveScopeRef(scopeRef, facetType, context.repertoireDir);
return existsSync(filePath) ? filePath : undefined;
}
return resolveFacetPathGeneric(name, buildCandidateDirsWithPackage(facetType, context));
@ -136,7 +136,7 @@ export function resolveFacetPath(
/**
* Resolve a facet name to its file content via 4-layer lookup.
*
* Handles @{owner}/{repo}/{facet-name} scope references when ensembleDir is provided.
* Handles @{owner}/{repo}/{facet-name} scope references when repertoireDir is provided.
*
* @returns File content if found, undefined otherwise.
*/
@ -165,9 +165,9 @@ export function resolveRefToContent(
facetType?: FacetType,
context?: FacetResolutionContext,
): string | undefined {
if (facetType && context && isScopeRef(ref) && context.ensembleDir) {
if (facetType && context && isScopeRef(ref) && context.repertoireDir) {
const scopeRef = parseScopeRef(ref);
const filePath = resolveScopeRef(scopeRef, facetType, context.ensembleDir);
const filePath = resolveScopeRef(scopeRef, facetType, context.repertoireDir);
return existsSync(filePath) ? readFileSync(filePath, 'utf-8') : undefined;
}
const candidateDirs = facetType && context
@ -201,9 +201,9 @@ export function resolvePersona(
pieceDir: string,
context?: FacetResolutionContext,
): { personaSpec?: string; personaPath?: string } {
if (rawPersona && isScopeRef(rawPersona) && context?.ensembleDir) {
if (rawPersona && isScopeRef(rawPersona) && context?.repertoireDir) {
const scopeRef = parseScopeRef(rawPersona);
const personaPath = resolveScopeRef(scopeRef, 'personas', context.ensembleDir);
const personaPath = resolveScopeRef(scopeRef, 'personas', context.repertoireDir);
return { personaSpec: rawPersona, personaPath: existsSync(personaPath) ? personaPath : undefined };
}
const candidateDirs = context

View File

@ -12,6 +12,7 @@ import type { Language } from '../../core/models/index.js';
import { getLanguageResourcesDir } from '../resources/index.js';
import type { FacetKind } from '../../faceted-prompting/index.js';
import { REPERTOIRE_DIR_NAME } from '../../features/repertoire/constants.js';
/** Facet types used in layer resolution */
export type { FacetKind as FacetType } from '../../faceted-prompting/index.js';
@ -105,25 +106,25 @@ export function getBuiltinFacetDir(lang: Language, facetType: FacetType): string
return join(getLanguageResourcesDir(lang), 'facets', facetType);
}
/** Get ensemble directory (~/.takt/ensemble/) */
export function getEnsembleDir(): string {
return join(getGlobalConfigDir(), 'ensemble');
/** Get repertoire directory (~/.takt/repertoire/) */
export function getRepertoireDir(): string {
return join(getGlobalConfigDir(), REPERTOIRE_DIR_NAME);
}
/** Get ensemble package directory (~/.takt/ensemble/@{owner}/{repo}/) */
export function getEnsemblePackageDir(owner: string, repo: string): string {
return join(getEnsembleDir(), `@${owner}`, repo);
/** Get repertoire package directory (~/.takt/repertoire/@{owner}/{repo}/) */
export function getRepertoirePackageDir(owner: string, repo: string): string {
return join(getRepertoireDir(), `@${owner}`, repo);
}
/**
* Get ensemble facet directory.
* Get repertoire facet directory.
*
* Defaults to the global ensemble dir when ensembleDir is not specified.
* Pass ensembleDir explicitly when resolving facets within a custom ensemble root
* Defaults to the global repertoire dir when repertoireDir is not specified.
* Pass repertoireDir explicitly when resolving facets within a custom repertoire root
* (e.g. the package-local resolution layer).
*/
export function getEnsembleFacetDir(owner: string, repo: string, facetType: FacetType, ensembleDir?: string): string {
const base = ensembleDir ?? getEnsembleDir();
export function getRepertoireFacetDir(owner: string, repo: string, facetType: FacetType, repertoireDir?: string): string {
const base = repertoireDir ?? getRepertoireDir();
return join(base, `@${owner}`, repo, 'facets', facetType);
}

View File

@ -97,7 +97,7 @@ export async function confirm(message: string, defaultYes = true): Promise<boole
const { useTty, forceTouchTty } = resolveTtyPolicy();
assertTtyIfForced(forceTouchTty);
if (!useTty) {
// Support piped stdin (e.g. echo "y" | takt ensemble add ...)
// Support piped stdin (e.g. echo "y" | takt repertoire add ...)
if (!process.stdin.isTTY && process.stdin.readable && !process.stdin.destroyed) {
return readConfirmFromPipe(defaultYes);
}