From eb42d91b77ef7248433b0b4420d2aa8822f78465 Mon Sep 17 00:00:00 2001 From: TautCony Date: Fri, 3 Jul 2026 23:55:35 +0800 Subject: [PATCH 01/41] feat: Update store selection process and enhance log service integration --- .codex/skills/openspec-apply-change/SKILL.md | 6 +- .../skills/openspec-archive-change/SKILL.md | 6 +- .../openspec-bulk-archive-change/SKILL.md | 248 ++++++++ .../skills/openspec-continue-change/SKILL.md | 121 ++++ .codex/skills/openspec-explore/SKILL.md | 4 +- .codex/skills/openspec-ff-change/SKILL.md | 104 ++++ .codex/skills/openspec-new-change/SKILL.md | 76 +++ .codex/skills/openspec-onboard/SKILL.md | 554 ++++++++++++++++++ .codex/skills/openspec-propose/SKILL.md | 4 +- .codex/skills/openspec-sync-specs/SKILL.md | 147 +++++ .codex/skills/openspec-verify-change/SKILL.md | 171 ++++++ .../.openspec.yaml | 2 + .../design.md | 84 +++ .../proposal.md | 29 + .../specs/avalonia-ui-shell/spec.md | 39 ++ .../supporting-ui-platform-services/spec.md | 32 + .../tasks.md | 36 ++ openspec/specs/avalonia-ui-shell/spec.md | 38 ++ .../supporting-ui-platform-services/spec.md | 31 + .../ChapterTool.Avalonia.csproj | 12 +- .../Localization/Resources/Strings.en-US.resx | 4 +- .../Localization/Resources/Strings.ja-JP.resx | 4 +- .../Localization/Resources/Strings.zh-CN.resx | 4 +- .../Services/AvaloniaWindowService.cs | 6 +- .../Services/RuntimeChapterLoadService.cs | 5 +- .../ViewModels/MainWindowViewModel.cs | 42 +- .../ViewModels/ToolWindowViewModels.cs | 17 + .../Views/MainWindow.axaml | 39 +- .../Views/MainWindow.axaml.cs | 88 +-- .../Services/IApplicationLogService.cs | 2 + .../Platform/ApplicationLogPanelProvider.cs | 4 + .../ChapterTool.Avalonia.Tests.csproj | 4 +- .../Headless/MainWindowHeadlessTests.cs | 19 + .../RuntimeChapterLoadServiceTests.cs | 7 +- .../ViewModels/MainWindowViewModelTests.cs | 85 ++- .../ViewModels/ToolWindowViewModelTests.cs | 15 + .../ChapterTool.Core.Tests.csproj | 2 +- .../ApplicationLogPanelProviderTests.cs | 54 ++ .../ChapterTool.Infrastructure.Tests.csproj | 2 +- 39 files changed, 2044 insertions(+), 103 deletions(-) create mode 100644 .codex/skills/openspec-bulk-archive-change/SKILL.md create mode 100644 .codex/skills/openspec-continue-change/SKILL.md create mode 100644 .codex/skills/openspec-ff-change/SKILL.md create mode 100644 .codex/skills/openspec-new-change/SKILL.md create mode 100644 .codex/skills/openspec-onboard/SKILL.md create mode 100644 .codex/skills/openspec-sync-specs/SKILL.md create mode 100644 .codex/skills/openspec-verify-change/SKILL.md create mode 100644 openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/.openspec.yaml create mode 100644 openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/design.md create mode 100644 openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/proposal.md create mode 100644 openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/specs/avalonia-ui-shell/spec.md create mode 100644 openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/specs/supporting-ui-platform-services/spec.md create mode 100644 openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/tasks.md diff --git a/.codex/skills/openspec-apply-change/SKILL.md b/.codex/skills/openspec-apply-change/SKILL.md index db4d8ce..0573792 100644 --- a/.codex/skills/openspec-apply-change/SKILL.md +++ b/.codex/skills/openspec-apply-change/SKILL.md @@ -6,11 +6,13 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.4.1" + generatedBy: "1.5.0" --- Implement tasks from an OpenSpec change. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -50,8 +52,6 @@ Implement tasks from an OpenSpec change. - If `state: "all_done"`: congratulate, suggest archive - Otherwise: proceed to implementation - **Workspace guard:** If status JSON reports `actionContext.mode: "workspace-planning"` and `allowedEditRoots` is empty, explain that full workspace apply is not supported in this slice. Treat linked repos and folders as read-only context, ask the user to select an affected area through an explicit implementation workflow, and STOP before editing files. - 4. **Read context files** Read every file path listed under `contextFiles` from the apply instructions output. diff --git a/.codex/skills/openspec-archive-change/SKILL.md b/.codex/skills/openspec-archive-change/SKILL.md index 97c3e5e..bb6a8cf 100644 --- a/.codex/skills/openspec-archive-change/SKILL.md +++ b/.codex/skills/openspec-archive-change/SKILL.md @@ -6,11 +6,13 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.4.1" + generatedBy: "1.5.0" --- Archive a completed change in the experimental workflow. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -33,8 +35,6 @@ Archive a completed change in the experimental workflow. - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context - `artifacts`: List of artifacts with their status (`done` or other) - If status reports `actionContext.mode: "workspace-planning"`, explain that workspace archive is not supported in this slice and STOP. Do not move workspace changes into repo-local archives or edit linked repos. - **If any artifacts are not `done`:** - Display warning listing incomplete artifacts - Use **AskUserQuestion tool** to confirm user wants to proceed diff --git a/.codex/skills/openspec-bulk-archive-change/SKILL.md b/.codex/skills/openspec-bulk-archive-change/SKILL.md new file mode 100644 index 0000000..1184181 --- /dev/null +++ b/.codex/skills/openspec-bulk-archive-change/SKILL.md @@ -0,0 +1,248 @@ +--- +name: openspec-bulk-archive-change +description: Archive multiple completed changes at once. Use when archiving several parallel changes. +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.5.0" +--- + +Archive multiple completed changes in a single operation. + +This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: None required (prompts for selection) + +**Steps** + +1. **Get active changes** + + Run `openspec list --json` to get all active changes. + + If no active changes exist, inform user and stop. + +2. **Prompt for change selection** + + Use **AskUserQuestion tool** with multi-select to let user choose changes: + - Show each change with its schema + - Include an option for "All changes" + - Allow any number of selections (1+ works, 2+ is the typical use case) + + **IMPORTANT**: Do NOT auto-select. Always let the user choose. + +3. **Batch validation - gather status for all selected changes** + + For each selected change, collect: + + a. **Artifact status** - Run `openspec status --change "" --json` + - Parse `schemaName`, `artifacts`, `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext` + - Note which artifacts are `done` vs other states + + b. **Task completion** - Read `artifactPaths.tasks.existingOutputPaths` from status JSON + - Count `- [ ]` (incomplete) vs `- [x]` (complete) + - If no tasks file exists, note as "No tasks" + + c. **Delta specs** - Check `artifactPaths.specs.existingOutputPaths` from status JSON + - List which capability specs exist + - For each, extract requirement names (lines matching `### Requirement: `) + +4. **Detect spec conflicts** + + Build a map of `capability -> [changes that touch it]`: + + ``` + auth -> [change-a, change-b] <- CONFLICT (2+ changes) + api -> [change-c] <- OK (only 1 change) + ``` + + A conflict exists when 2+ selected changes have delta specs for the same capability. + +5. **Resolve conflicts agentically** + + **For each conflict**, investigate the codebase: + + a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify + + b. **Search the codebase** for implementation evidence: + - Look for code implementing requirements from each delta spec + - Check for related files, functions, or tests + + c. **Determine resolution**: + - If only one change is actually implemented -> sync that one's specs + - If both implemented -> apply in chronological order (older first, newer overwrites) + - If neither implemented -> skip spec sync, warn user + + d. **Record resolution** for each conflict: + - Which change's specs to apply + - In what order (if both) + - Rationale (what was found in codebase) + +6. **Show consolidated status table** + + Display a table summarizing all changes: + + ``` + | Change | Artifacts | Tasks | Specs | Conflicts | Status | + |---------------------|-----------|-------|---------|-----------|--------| + | schema-management | Done | 5/5 | 2 delta | None | Ready | + | project-config | Done | 3/3 | 1 delta | None | Ready | + | add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* | + | add-verify-skill | 1 left | 2/5 | None | None | Warn | + ``` + + For conflicts, show the resolution: + ``` + * Conflict resolution: + - auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order) + ``` + + For incomplete changes, show warnings: + ``` + Warnings: + - add-verify-skill: 1 incomplete artifact, 3 incomplete tasks + ``` + +7. **Confirm batch operation** + + Use **AskUserQuestion tool** with a single confirmation: + + - "Archive N changes?" with options based on status + - Options might include: + - "Archive all N changes" + - "Archive only N ready changes (skip incomplete)" + - "Cancel" + + If there are incomplete changes, make clear they'll be archived with warnings. + +8. **Execute archive for each confirmed change** + + Process changes in the determined order (respecting conflict resolution): + + a. **Sync specs** if delta specs exist: + - Use the openspec-sync-specs approach (agent-driven intelligent merge) + - For conflicts, apply in resolved order + - Track if sync was done + + b. **Perform the archive**: + ```bash + mkdir -p "/archive" + mv "" "/archive/YYYY-MM-DD-" + ``` + + c. **Track outcome** for each change: + - Success: archived successfully + - Failed: error during archive (record error) + - Skipped: user chose not to archive (if applicable) + +9. **Display summary** + + Show final results: + + ``` + ## Bulk Archive Complete + + Archived 3 changes: + - schema-management-cli -> archive/2026-01-19-schema-management-cli/ + - project-config -> archive/2026-01-19-project-config/ + - add-oauth -> archive/2026-01-19-add-oauth/ + + Skipped 1 change: + - add-verify-skill (user chose not to archive incomplete) + + Spec sync summary: + - 4 delta specs synced to main specs + - 1 conflict resolved (auth: applied both in chronological order) + ``` + + If any failures: + ``` + Failed 1 change: + - some-change: Archive directory already exists + ``` + +**Conflict Resolution Examples** + +Example 1: Only one implemented +``` +Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt] + +Checking add-oauth: +- Delta adds "OAuth Provider Integration" requirement +- Searching codebase... found src/auth/oauth.ts implementing OAuth flow + +Checking add-jwt: +- Delta adds "JWT Token Handling" requirement +- Searching codebase... no JWT implementation found + +Resolution: Only add-oauth is implemented. Will sync add-oauth specs only. +``` + +Example 2: Both implemented +``` +Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql] + +Checking add-rest-api (created 2026-01-10): +- Delta adds "REST Endpoints" requirement +- Searching codebase... found src/api/rest.ts + +Checking add-graphql (created 2026-01-15): +- Delta adds "GraphQL Schema" requirement +- Searching codebase... found src/api/graphql.ts + +Resolution: Both implemented. Will apply add-rest-api specs first, +then add-graphql specs (chronological order, newer takes precedence). +``` + +**Output On Success** + +``` +## Bulk Archive Complete + +Archived N changes: +- -> archive/YYYY-MM-DD-/ +- -> archive/YYYY-MM-DD-/ + +Spec sync summary: +- N delta specs synced to main specs +- No conflicts (or: M conflicts resolved) +``` + +**Output On Partial Success** + +``` +## Bulk Archive Complete (partial) + +Archived N changes: +- -> archive/YYYY-MM-DD-/ + +Skipped M changes: +- (user chose not to archive incomplete) + +Failed K changes: +- : Archive directory already exists +``` + +**Output When No Changes** + +``` +## No Changes to Archive + +No active changes found. Create a new change to get started. +``` + +**Guardrails** +- Allow any number of changes (1+ is fine, 2+ is the typical use case) +- Always prompt for selection, never auto-select +- Detect spec conflicts early and resolve by checking codebase +- When both changes are implemented, apply specs in chronological order +- Skip spec sync only when implementation is missing (warn user) +- Show clear per-change status before confirming +- Use single confirmation for entire batch +- Track and report all outcomes (success/skip/fail) +- Preserve .openspec.yaml when moving to archive +- Archive directory target uses current date: YYYY-MM-DD- +- If archive target exists, fail that change but continue with others diff --git a/.codex/skills/openspec-continue-change/SKILL.md b/.codex/skills/openspec-continue-change/SKILL.md new file mode 100644 index 0000000..76a33b6 --- /dev/null +++ b/.codex/skills/openspec-continue-change/SKILL.md @@ -0,0 +1,121 @@ +--- +name: openspec-continue-change +description: Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow. +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.5.0" +--- + +Continue working on a change by creating the next artifact. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **If no change name provided, prompt for selection** + + Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on. + + Present the top 3-4 most recently modified changes as options, showing: + - Change name + - Schema (from `schema` field if present, otherwise "spec-driven") + - Status (e.g., "0/5 tasks", "complete", "no tasks") + - How recently it was modified (from `lastModified` field) + + Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue. + + **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + +2. **Check current status** + ```bash + openspec status --change "" --json + ``` + Parse the JSON to understand current state. The response includes: + - `schemaName`: The workflow schema being used (e.g., "spec-driven") + - `artifacts`: Array of artifacts with their status ("done", "ready", "blocked") + - `isComplete`: Boolean indicating if all artifacts are complete + - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. + +3. **Act based on status**: + + --- + + **If all artifacts are complete (`isComplete: true`)**: + - Congratulate the user + - Show final status including the schema used + - Suggest: "All artifacts created! You can now implement this change or archive it." + - STOP + + --- + + **If artifacts are ready to create** (status shows artifacts with `status: "ready"`): + - Pick the FIRST artifact with `status: "ready"` from the status output + - Get its instructions: + ```bash + openspec instructions --change "" --json + ``` + - Parse the JSON. The key fields are: + - `context`: Project background (constraints for you - do NOT include in output) + - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) + - `template`: The structure to use for your output file + - `instruction`: Schema-specific guidance + - `resolvedOutputPath`: Resolved path or pattern to write the artifact + - `dependencies`: Completed artifacts to read for context + - **Create the artifact file**: + - Read any completed dependency files for context + - Use `template` as the structure - fill in its sections + - Apply `context` and `rules` as constraints when writing - but do NOT copy them into the file + - Write to the `resolvedOutputPath` specified in instructions. If it is a glob pattern, choose the concrete file path using the schema instruction and the change's context + - Show what was created and what's now unlocked + - STOP after creating ONE artifact + + --- + + **If no artifacts are ready (all blocked)**: + - This shouldn't happen with a valid schema + - Show status and suggest checking for issues + +4. **After creating an artifact, show progress** + ```bash + openspec status --change "" + ``` + +**Output** + +After each invocation, show: +- Which artifact was created +- Schema workflow being used +- Current progress (N/M complete) +- What artifacts are now unlocked +- Prompt: "Want to continue? Just ask me to continue or tell me what to do next." + +**Artifact Creation Guidelines** + +The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create. + +Common artifact patterns: + +**spec-driven schema** (proposal → specs → design → tasks): +- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact. + - The Capabilities section is critical - each capability listed will need a spec file. +- **specs//spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name). +- **design.md**: Document technical decisions, architecture, and implementation approach. +- **tasks.md**: Break down implementation into checkboxed tasks. + +For other schemas, follow the `instruction` field from the CLI output. + +**Guardrails** +- Create ONE artifact per invocation +- Always read dependency artifacts before creating a new one +- Never skip artifacts or create out of order +- If context is unclear, ask the user before creating +- Verify the artifact file exists after writing before marking progress +- Use the schema's artifact sequence, don't assume specific artifact names +- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file + - Do NOT copy ``, ``, `` blocks into the artifact + - These guide what you write, but should never appear in the output diff --git a/.codex/skills/openspec-explore/SKILL.md b/.codex/skills/openspec-explore/SKILL.md index 1e97aaa..756b31c 100644 --- a/.codex/skills/openspec-explore/SKILL.md +++ b/.codex/skills/openspec-explore/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.4.1" + generatedBy: "1.5.0" --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. @@ -15,6 +15,8 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + --- ## The Stance diff --git a/.codex/skills/openspec-ff-change/SKILL.md b/.codex/skills/openspec-ff-change/SKILL.md new file mode 100644 index 0000000..a345b30 --- /dev/null +++ b/.codex/skills/openspec-ff-change/SKILL.md @@ -0,0 +1,104 @@ +--- +name: openspec-ff-change +description: Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually. +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.5.0" +--- + +Fast-forward through artifact creation - generate everything needed to start implementation in one go. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. + +**Steps** + +1. **If no clear input provided, ask what they want to build** + + Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: + > "What change do you want to work on? Describe what you want to build or fix." + + From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). + + **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. + +2. **Create the change directory** + ```bash + openspec new change "" + ``` + This creates a scaffolded change in the planning home resolved by the CLI. + +3. **Get the artifact build order** + ```bash + openspec status --change "" --json + ``` + Parse the JSON to get: + - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`) + - `artifacts`: list of all artifacts with their status and dependencies + - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. + +4. **Create artifacts in sequence until apply-ready** + + Use the **TodoWrite tool** to track progress through the artifacts. + + Loop through artifacts in dependency order (artifacts with no pending dependencies first): + + a. **For each artifact that is `ready` (dependencies satisfied)**: + - Get instructions: + ```bash + openspec instructions --change "" --json + ``` + - The instructions JSON includes: + - `context`: Project background (constraints for you - do NOT include in output) + - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) + - `template`: The structure to use for your output file + - `instruction`: Schema-specific guidance for this artifact type + - `resolvedOutputPath`: Resolved path or pattern to write the artifact + - `dependencies`: Completed artifacts to read for context + - Read any completed dependency files for context + - Create the artifact file using `template` as the structure and write it to `resolvedOutputPath` + - Apply `context` and `rules` as constraints - but do NOT copy them into the file + - Show brief progress: "✓ Created " + + b. **Continue until all `applyRequires` artifacts are complete** + - After creating each artifact, re-run `openspec status --change "" --json` + - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array + - Stop when all `applyRequires` artifacts are done + + c. **If an artifact requires user input** (unclear context): + - Use **AskUserQuestion tool** to clarify + - Then continue with creation + +5. **Show final status** + ```bash + openspec status --change "" + ``` + +**Output** + +After completing all artifacts, summarize: +- Change name and location +- List of artifacts created with brief descriptions +- What's ready: "All artifacts created! Ready for implementation." +- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks." + +**Artifact Creation Guidelines** + +- Follow the `instruction` field from `openspec instructions` for each artifact type +- The schema defines what each artifact should contain - follow it +- Read dependency artifacts for context before creating new ones +- Use `template` as the structure for your output file - fill in its sections +- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file + - Do NOT copy ``, ``, `` blocks into the artifact + - These guide what you write, but should never appear in the output + +**Guardrails** +- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) +- Always read dependency artifacts before creating a new one +- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum +- If a change with that name already exists, suggest continuing that change instead +- Verify each artifact file exists after writing before proceeding to next diff --git a/.codex/skills/openspec-new-change/SKILL.md b/.codex/skills/openspec-new-change/SKILL.md new file mode 100644 index 0000000..091c4e2 --- /dev/null +++ b/.codex/skills/openspec-new-change/SKILL.md @@ -0,0 +1,76 @@ +--- +name: openspec-new-change +description: Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach. +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.5.0" +--- + +Start a new change using the experimental artifact-driven approach. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. + +**Steps** + +1. **If no clear input provided, ask what they want to build** + + Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: + > "What change do you want to work on? Describe what you want to build or fix." + + From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). + + **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. + +2. **Determine the workflow schema** + + Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow. + + **Use a different schema only if the user mentions:** + - A specific schema name → use `--schema ` + - "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose + + **Otherwise**: Omit `--schema` to use the default. + +3. **Create the change directory** + ```bash + openspec new change "" + ``` + Add `--schema ` only if the user requested a specific workflow. + This creates a scaffolded change in the planning home resolved by the CLI. + +4. **Show the artifact status** + ```bash + openspec status --change "" --json + ``` + Use the returned `planningHome`, `changeRoot`, `artifactPaths`, and `nextSteps` instead of assuming repo-local paths. + +5. **Get instructions for the first artifact** + The first artifact depends on the schema (e.g., `proposal` for spec-driven). + Check the status output to find the first artifact with status "ready". + ```bash + openspec instructions --change "" + ``` + This outputs the template and context for creating the first artifact. + +6. **STOP and wait for user direction** + +**Output** + +After completing the steps, summarize: +- Change name and location +- Schema/workflow being used and its artifact sequence +- Current status (0/N artifacts complete) +- The template for the first artifact +- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue." + +**Guardrails** +- Do NOT create any artifacts yet - just show the instructions +- Do NOT advance beyond showing the first artifact template +- If the name is invalid (not kebab-case), ask for a valid name +- If a change with that name already exists, suggest continuing that change instead +- Pass --schema if using a non-default workflow diff --git a/.codex/skills/openspec-onboard/SKILL.md b/.codex/skills/openspec-onboard/SKILL.md new file mode 100644 index 0000000..bc791d5 --- /dev/null +++ b/.codex/skills/openspec-onboard/SKILL.md @@ -0,0 +1,554 @@ +--- +name: openspec-onboard +description: Guided onboarding for OpenSpec - walk through a complete workflow cycle with narration and real codebase work. +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.5.0" +--- + +Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +--- + +## Preflight + +Before starting, check if the OpenSpec CLI is installed: + +```bash +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } +``` + +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`. + +Stop here if not installed. + +--- + +## Phase 1: Welcome + +Display: + +``` +## Welcome to OpenSpec! + +I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it. + +**What we'll do:** +1. Pick a small, real task in your codebase +2. Explore the problem briefly +3. Create a change (the container for our work) +4. Build the artifacts: proposal → specs → design → tasks +5. Implement the tasks +6. Archive the completed change + +**Time:** ~15-20 minutes + +Let's start by finding something to work on. +``` + +--- + +## Phase 2: Task Selection + +### Codebase Analysis + +Scan the codebase for small improvement opportunities. Look for: + +1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files +2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch +3. **Functions without tests** - Cross-reference `src/` with test directories +4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`) +5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code +6. **Missing validation** - User input handlers without validation + +Also check recent git activity: +```bash +# Unix/macOS +git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } +``` + +### Present Suggestions + +From your analysis, present 3-4 specific suggestions: + +``` +## Task Suggestions + +Based on scanning your codebase, here are some good starter tasks: + +**1. [Most promising task]** + Location: `src/path/to/file.ts:42` + Scope: ~1-2 files, ~20-30 lines + Why it's good: [brief reason] + +**2. [Second task]** + Location: `src/another/file.ts` + Scope: ~1 file, ~15 lines + Why it's good: [brief reason] + +**3. [Third task]** + Location: [location] + Scope: [estimate] + Why it's good: [brief reason] + +**4. Something else?** + Tell me what you'd like to work on. + +Which task interests you? (Pick a number or describe your own) +``` + +**If nothing found:** Fall back to asking what the user wants to build: +> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix? + +### Scope Guardrail + +If the user picks or describes something too large (major feature, multi-day work): + +``` +That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through. + +For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details. + +**Options:** +1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]? +2. **Pick something else** - One of the other suggestions, or a different small task? +3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer. + +What would you prefer? +``` + +Let the user override if they insist—this is a soft guardrail. + +--- + +## Phase 3: Explore Demo + +Once a task is selected, briefly demonstrate explore mode: + +``` +Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction. +``` + +Spend 1-2 minutes investigating the relevant code: +- Read the file(s) involved +- Draw a quick ASCII diagram if it helps +- Note any considerations + +``` +## Quick Exploration + +[Your brief analysis—what you found, any considerations] + +┌─────────────────────────────────────────┐ +│ [Optional: ASCII diagram if helpful] │ +└─────────────────────────────────────────┘ + +Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem. + +Now let's create a change to hold our work. +``` + +**PAUSE** - Wait for user acknowledgment before proceeding. + +--- + +## Phase 4: Create the Change + +**EXPLAIN:** +``` +## Creating a Change + +A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives at the `changeRoot` reported by `openspec status --change "" --json` and holds your artifacts—proposal, specs, design, tasks. + +Let me create one for our task. +``` + +**DO:** Create the change with a derived kebab-case name: +```bash +openspec new change "" +``` + +**SHOW:** +``` +Created: + +The folder structure: +``` +/ +├── proposal.md ← Why we're doing this (empty, we'll fill it) +├── design.md ← How we'll build it (empty) +├── specs/ ← Detailed requirements (empty) +└── tasks.md ← Implementation checklist (empty) +``` + +Now let's fill in the first artifact—the proposal. +``` + +--- + +## Phase 5: Proposal + +**EXPLAIN:** +``` +## The Proposal + +The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work. + +I'll draft one based on our task. +``` + +**DO:** Draft the proposal content (don't save yet): + +``` +Here's a draft proposal: + +--- + +## Why + +[1-2 sentences explaining the problem/opportunity] + +## What Changes + +[Bullet points of what will be different] + +## Capabilities + +### New Capabilities +- ``: [brief description] + +### Modified Capabilities + + +## Impact + +- `src/path/to/file.ts`: [what changes] +- [other files if applicable] + +--- + +Does this capture the intent? I can adjust before we save it. +``` + +**PAUSE** - Wait for user approval/feedback. + +After approval, save the proposal: +```bash +openspec instructions proposal --change "" --json +``` +Then write the content to the `resolvedOutputPath` from `openspec instructions proposal --change "" --json`. + +``` +Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves. + +Next up: specs. +``` + +--- + +## Phase 6: Specs + +**EXPLAIN:** +``` +## Specs + +Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear. + +For a small task like this, we might only need one spec file. +``` + +**DO:** Resolve where the spec file should be created: +```bash +openspec instructions specs --change "" --json +# Use resolvedOutputPath from the JSON. If it is a glob, choose the concrete file path using the schema instruction and the change's context. +``` + +Draft the spec content: + +``` +Here's the spec: + +--- + +## ADDED Requirements + +### Requirement: + + + +#### Scenario: + +- **WHEN** +- **THEN** +- **AND** + +--- + +This format—WHEN/THEN/AND—makes requirements testable. You can literally read them as test cases. +``` + +Save to the concrete file path chosen from `resolvedOutputPath`. + +--- + +## Phase 7: Design + +**EXPLAIN:** +``` +## Design + +The design captures **how** we'll build it—technical decisions, tradeoffs, approach. + +For small changes, this might be brief. That's fine—not every change needs deep design discussion. +``` + +**DO:** Draft design.md: + +``` +Here's the design: + +--- + +## Context + +[Brief context about the current state] + +## Goals / Non-Goals + +**Goals:** +- [What we're trying to achieve] + +**Non-Goals:** +- [What's explicitly out of scope] + +## Decisions + +### Decision 1: [Key decision] + +[Explanation of approach and rationale] + +--- + +For a small task, this captures the key decisions without over-engineering. +``` + +Save to the `resolvedOutputPath` from `openspec instructions design --change "" --json`. + +--- + +## Phase 8: Tasks + +**EXPLAIN:** +``` +## Tasks + +Finally, we break the work into implementation tasks—checkboxes that drive the apply phase. + +These should be small, clear, and in logical order. +``` + +**DO:** Generate tasks based on specs and design: + +``` +Here are the implementation tasks: + +--- + +## 1. [Category or file] + +- [ ] 1.1 [Specific task] +- [ ] 1.2 [Specific task] + +## 2. Verify + +- [ ] 2.1 [Verification step] + +--- + +Each checkbox becomes a unit of work in the apply phase. Ready to implement? +``` + +**PAUSE** - Wait for user to confirm they're ready to implement. + +Save to the `resolvedOutputPath` from `openspec instructions tasks --change "" --json`. + +--- + +## Phase 9: Apply (Implementation) + +**EXPLAIN:** +``` +## Implementation + +Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach. +``` + +**DO:** For each task: + +1. Announce: "Working on task N: [description]" +2. Implement the change in the codebase +3. Reference specs/design naturally: "The spec says X, so I'm doing Y" +4. Mark complete in tasks.md: `- [ ]` → `- [x]` +5. Brief status: "✓ Task N complete" + +Keep narration light—don't over-explain every line of code. + +After all tasks: + +``` +## Implementation Complete + +All tasks done: +- [x] Task 1 +- [x] Task 2 +- [x] ... + +The change is implemented! One more step—let's archive it. +``` + +--- + +## Phase 10: Archive + +**EXPLAIN:** +``` +## Archiving + +When a change is complete, we archive it. The archive path is derived from `planningHome.changesDir` and the date. + +Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way. +``` + +**DO:** +```bash +openspec archive "" +``` + +**SHOW:** +``` +Archived to: `/archive/YYYY-MM-DD-/` + +The change is now part of your project's history. The code is in your codebase, the decision record is preserved. +``` + +--- + +## Phase 11: Recap & Next Steps + +``` +## Congratulations! + +You just completed a full OpenSpec cycle: + +1. **Explore** - Thought through the problem +2. **New** - Created a change container +3. **Proposal** - Captured WHY +4. **Specs** - Defined WHAT in detail +5. **Design** - Decided HOW +6. **Tasks** - Broke it into steps +7. **Apply** - Implemented the work +8. **Archive** - Preserved the record + +This same rhythm works for any size change—a small fix or a major feature. + +--- + +## Command Reference + +**Core workflow:** + + | Command | What it does | + |-------------------|--------------------------------------------| + | `/opsx:propose` | Create a change and generate all artifacts | + | `/opsx:explore` | Think through problems before/during work | + | `/opsx:apply` | Implement tasks from a change | + | `/opsx:archive` | Archive a completed change | + +**Additional commands:** + + | Command | What it does | + |--------------------|----------------------------------------------------------| + | `/opsx:new` | Start a new change, step through artifacts one at a time | + | `/opsx:continue` | Continue working on an existing change | + | `/opsx:ff` | Fast-forward: create all artifacts at once | + | `/opsx:verify` | Verify implementation matches artifacts | + +--- + +## What's Next? + +Try `/opsx:propose` on something you actually want to build. You've got the rhythm now! +``` + +--- + +## Graceful Exit Handling + +### User wants to stop mid-way + +If the user says they need to stop, want to pause, or seem disengaged: + +``` +No problem! Your change is saved at the `changeRoot` reported by `openspec status --change "" --json`. + +To pick up where we left off later: +- `/opsx:continue ` - Resume artifact creation +- `/opsx:apply ` - Jump to implementation (if tasks exist) + +The work won't be lost. Come back whenever you're ready. +``` + +Exit gracefully without pressure. + +### User just wants command reference + +If the user says they just want to see the commands or skip the tutorial: + +``` +## OpenSpec Quick Reference + +**Core workflow:** + + | Command | What it does | + |--------------------------|--------------------------------------------| + | `/opsx:propose ` | Create a change and generate all artifacts | + | `/opsx:explore` | Think through problems (no code changes) | + | `/opsx:apply ` | Implement tasks | + | `/opsx:archive ` | Archive when done | + +**Additional commands:** + + | Command | What it does | + |---------------------------|-------------------------------------| + | `/opsx:new ` | Start a new change, step by step | + | `/opsx:continue ` | Continue an existing change | + | `/opsx:ff ` | Fast-forward: all artifacts at once | + | `/opsx:verify ` | Verify implementation | + +Try `/opsx:propose` to start your first change. +``` + +Exit gracefully. + +--- + +## Guardrails + +- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive) +- **Keep narration light** during implementation—teach without lecturing +- **Don't skip phases** even if the change is small—the goal is teaching the workflow +- **Pause for acknowledgment** at marked points, but don't over-pause +- **Handle exits gracefully**—never pressure the user to continue +- **Use real codebase tasks**—don't simulate or use fake examples +- **Adjust scope gently**—guide toward smaller tasks but respect user choice diff --git a/.codex/skills/openspec-propose/SKILL.md b/.codex/skills/openspec-propose/SKILL.md index 9fc8513..0208e03 100644 --- a/.codex/skills/openspec-propose/SKILL.md +++ b/.codex/skills/openspec-propose/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.4.1" + generatedBy: "1.5.0" --- Propose a new change - create the change and generate all artifacts in one step. @@ -20,6 +20,8 @@ When ready to implement, run /opsx:apply --- +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. **Steps** diff --git a/.codex/skills/openspec-sync-specs/SKILL.md b/.codex/skills/openspec-sync-specs/SKILL.md new file mode 100644 index 0000000..f4ec054 --- /dev/null +++ b/.codex/skills/openspec-sync-specs/SKILL.md @@ -0,0 +1,147 @@ +--- +name: openspec-sync-specs +description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change. +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.5.0" +--- + +Sync delta specs from a change to main specs. + +This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement). + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **If no change name provided, prompt for selection** + + Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. + + Show changes that have delta specs (under `specs/` directory). + + **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + +2. **Resolve change context** + + Run: + ```bash + openspec status --change "" --json + ``` + +3. **Find delta specs** + + Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the list of delta spec files. + + Each delta spec file contains sections like: + - `## ADDED Requirements` - New requirements to add + - `## MODIFIED Requirements` - Changes to existing requirements + - `## REMOVED Requirements` - Requirements to remove + - `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format) + + If no delta specs found, inform user and stop. + +4. **For each delta spec, apply changes to main specs** + + For each repo-local capability delta spec path returned by the CLI: + + a. **Read the delta spec** to understand the intended changes + + b. **Read the main spec** at `openspec/specs//spec.md` (may not exist yet) + + c. **Apply changes intelligently**: + + **ADDED Requirements:** + - If requirement doesn't exist in main spec → add it + - If requirement already exists → update it to match (treat as implicit MODIFIED) + + **MODIFIED Requirements:** + - Find the requirement in main spec + - Apply the changes - this can be: + - Adding new scenarios (don't need to copy existing ones) + - Modifying existing scenarios + - Changing the requirement description + - Preserve scenarios/content not mentioned in the delta + + **REMOVED Requirements:** + - Remove the entire requirement block from main spec + + **RENAMED Requirements:** + - Find the FROM requirement, rename to TO + + d. **Create new main spec** if capability doesn't exist yet: + - Create `openspec/specs//spec.md` + - Add Purpose section (can be brief, mark as TBD) + - Add Requirements section with the ADDED requirements + +5. **Show summary** + + After applying all changes, summarize: + - Which capabilities were updated + - What changes were made (requirements added/modified/removed/renamed) + +**Delta Spec Format Reference** + +```markdown +## ADDED Requirements + +### Requirement: New Feature +The system SHALL do something new. + +#### Scenario: Basic case +- **WHEN** user does X +- **THEN** system does Y + +## MODIFIED Requirements + +### Requirement: Existing Feature +#### Scenario: New scenario to add +- **WHEN** user does A +- **THEN** system does B + +## REMOVED Requirements + +### Requirement: Deprecated Feature + +## RENAMED Requirements + +- FROM: `### Requirement: Old Name` +- TO: `### Requirement: New Name` +``` + +**Key Principle: Intelligent Merging** + +Unlike programmatic merging, you can apply **partial updates**: +- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios +- The delta represents *intent*, not a wholesale replacement +- Use your judgment to merge changes sensibly + +**Output On Success** + +``` +## Specs Synced: + +Updated main specs: + +****: +- Added requirement: "New Feature" +- Modified requirement: "Existing Feature" (added 1 scenario) + +****: +- Created new spec file +- Added requirement: "Another Feature" + +Main specs are now updated. The change remains active - archive when implementation is complete. +``` + +**Guardrails** +- Read both delta and main specs before making changes +- Preserve existing content not mentioned in delta +- If something is unclear, ask for clarification +- Show what you're changing as you go +- The operation should be idempotent - running twice should give same result diff --git a/.codex/skills/openspec-verify-change/SKILL.md b/.codex/skills/openspec-verify-change/SKILL.md new file mode 100644 index 0000000..8d11108 --- /dev/null +++ b/.codex/skills/openspec-verify-change/SKILL.md @@ -0,0 +1,171 @@ +--- +name: openspec-verify-change +description: Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving. +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.5.0" +--- + +Verify that an implementation matches the change artifacts (specs, tasks, design). + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **If no change name provided, prompt for selection** + + Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. + + Show changes that have implementation tasks (tasks artifact exists). + Include the schema used for each change if available. + Mark changes with incomplete tasks as "(In Progress)". + + **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + +2. **Check status to understand the schema** + ```bash + openspec status --change "" --json + ``` + Parse the JSON to understand: + - `schemaName`: The workflow being used (e.g., "spec-driven") + - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context + - Which artifacts exist for this change + +3. **Get planning context and load artifacts** + + ```bash + openspec instructions apply --change "" --json + ``` + + This returns the change directory and `contextFiles` (artifact ID -> array of concrete file paths). Read all available artifacts from `contextFiles`. + +4. **Initialize verification report structure** + + Create a report structure with three dimensions: + - **Completeness**: Track tasks and spec coverage + - **Correctness**: Track requirement implementation and scenario coverage + - **Coherence**: Track design adherence and pattern consistency + + Each dimension can have CRITICAL, WARNING, or SUGGESTION issues. + +5. **Verify Completeness** + + **Task Completion**: + - If `contextFiles.tasks` exists, read every file path in it + - Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete) + - Count complete vs total tasks + - If incomplete tasks exist: + - Add CRITICAL issue for each incomplete task + - Recommendation: "Complete task: " or "Mark as done if already implemented" + + **Spec Coverage**: + - If delta specs exist in `contextFiles.specs`: + - Extract all requirements (marked with "### Requirement:") + - For each requirement: + - Search codebase for keywords related to the requirement + - Assess if implementation likely exists + - If requirements appear unimplemented: + - Add CRITICAL issue: "Requirement not found: " + - Recommendation: "Implement requirement X: " + +6. **Verify Correctness** + + **Requirement Implementation Mapping**: + - For each requirement from delta specs: + - Search codebase for implementation evidence + - If found, note file paths and line ranges + - Assess if implementation matches requirement intent + - If divergence detected: + - Add WARNING: "Implementation may diverge from spec:
" + - Recommendation: "Review : against requirement X" + + **Scenario Coverage**: + - For each scenario in delta specs (marked with "#### Scenario:"): + - Check if conditions are handled in code + - Check if tests exist covering the scenario + - If scenario appears uncovered: + - Add WARNING: "Scenario not covered: " + - Recommendation: "Add test or implementation for scenario: " + +7. **Verify Coherence** + + **Design Adherence**: + - If `contextFiles.design` exists: + - Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:") + - Verify implementation follows those decisions + - If contradiction detected: + - Add WARNING: "Design decision not followed: " + - Recommendation: "Update implementation or revise design.md to match reality" + - If no design.md: Skip design adherence check, note "No design.md to verify against" + + **Code Pattern Consistency**: + - Review new code for consistency with project patterns + - Check file naming, directory structure, coding style + - If significant deviations found: + - Add SUGGESTION: "Code pattern deviation:
" + - Recommendation: "Consider following project pattern: " + +8. **Generate Verification Report** + + **Summary Scorecard**: + ``` + ## Verification Report: + + ### Summary + | Dimension | Status | + |--------------|------------------| + | Completeness | X/Y tasks, N reqs| + | Correctness | M/N reqs covered | + | Coherence | Followed/Issues | + ``` + + **Issues by Priority**: + + 1. **CRITICAL** (Must fix before archive): + - Incomplete tasks + - Missing requirement implementations + - Each with specific, actionable recommendation + + 2. **WARNING** (Should fix): + - Spec/design divergences + - Missing scenario coverage + - Each with specific recommendation + + 3. **SUGGESTION** (Nice to fix): + - Pattern inconsistencies + - Minor improvements + - Each with specific recommendation + + **Final Assessment**: + - If CRITICAL issues: "X critical issue(s) found. Fix before archiving." + - If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)." + - If all clear: "All checks passed. Ready for archive." + +**Verification Heuristics** + +- **Completeness**: Focus on objective checklist items (checkboxes, requirements list) +- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty +- **Coherence**: Look for glaring inconsistencies, don't nitpick style +- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL +- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable + +**Graceful Degradation** + +- If only tasks.md exists: verify task completion only, skip spec/design checks +- If tasks + specs exist: verify completeness and correctness, skip design +- If full artifacts: verify all three dimensions +- Always note which checks were skipped and why + +**Output Format** + +Use clear markdown with: +- Table for summary scorecard +- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION) +- Code references in format: `file.ts:123` +- Specific, actionable recommendations +- No vague suggestions like "consider reviewing" diff --git a/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/.openspec.yaml b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/.openspec.yaml new file mode 100644 index 0000000..43e65ca --- /dev/null +++ b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-03 diff --git a/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/design.md b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/design.md new file mode 100644 index 0000000..649a203 --- /dev/null +++ b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/design.md @@ -0,0 +1,84 @@ +## Context + +`docs/todo-list.md` is an architectural review checklist derived from legacy WinForms patterns. The current Avalonia/Core/Infrastructure solution already follows several of its recommendations: + +- `MainWindowViewModel` is constructed with `IFilePickerService`, `IWindowService`, `IApplicationLogService`, `ILogger`, settings, shell, editing, import, export, and transform services; it does not expose a `SetMainWindow(Window)` pattern. +- `RuntimeChapterLoadService` resolves importers through `IChapterImporterRegistry` and fallback logic instead of putting extension switches in the ViewModel. +- `DialogRequest`, `IDialogService`, `IWindowService`, and platform service interfaces keep Core and ViewModels away from static UI notification callbacks. +- `ApplicationLogPanelProvider` is bounded, structured, and backed by `Microsoft.Extensions.Logging`. + +The remaining hardening work is mostly about making these boundaries observable and regression-resistant. `ApplicationLogPanelProvider` currently exposes snapshots and formatting, but no event that lets the log window subscribe to new entries. `MainWindow.axaml.cs` still performs some routine state synchronization through `Refresh()`, direct selector assignments, and manual command-state raises. Loading is async and awaited, but the contract that importer work must not mutate observable UI collections should be explicit and tested. + +## Goals / Non-Goals + +**Goals:** + +- Add a bounded, evented UI log surface that supports both history replay and incremental updates. +- Keep structured `ILogger` as the source of diagnostic events while giving the log window a simple event-driven subscription model. +- Reduce main-window code-behind responsibility for routine UI state by moving state, command availability, and option synchronization into observable ViewModel properties and commands. +- Make async load/update threading expectations explicit: importers may perform background IO, but `ObservableCollection` updates happen in ViewModel-controlled UI flow. +- Preserve importer registry/fallback extensibility and platform-service boundaries. +- Add focused tests that prevent regressions without static source-string assertions over implementation files. + +**Non-Goals:** + +- No parser, exporter, chapter model, or file format behavior changes. +- No replacement of the existing OpenSpec capabilities or broad UI redesign. +- No new external logging framework beyond the existing standard logging/Serilog setup. +- No attempt to remove every view-specific code-behind event adapter; file pickers, drag/drop, keyboard adaptation, and layout adaptation can remain view responsibilities. + +## Decisions + +1. Keep `IApplicationLogService` as the UI log abstraction and extend it with evented behavior. + + The log window needs three things: a snapshot for history, an event for new entries, and a clear operation. Extending the existing service keeps the surface small and aligns with `ApplicationLogPanelProvider` already implementing both `IApplicationLogService` and `ILoggerProvider`. + + Alternative considered: introduce a separate `IApplicationLogObservable`. That would make read/subscribe responsibilities explicit, but it adds a second abstraction for the same UI sink and more composition wiring. A separate interface can be introduced later if non-UI sinks need different lifecycle semantics. + +2. Emit events after entries are stored and trimmed. + + Subscribers should see the same entries that `Entries` would expose after capacity enforcement. The event payload should include the newly accepted `ApplicationLogEntry`; subscribers that need full state after a clear can ask for `Entries`. + + Alternative considered: emit before adding the entry. That makes the event lower latency but creates race-prone behavior where a log window receiving the event cannot yet see the entry in the snapshot. + +3. Treat localization as display-time formatting, not event-time rendering. + + Existing `ApplicationLogEntry` supports message keys, arguments, technical detail, category, severity, and exception text. The event should carry the structured entry, and the log view/model should render with the active localizer when displayed. + + Alternative considered: store only rendered text. That is simpler for append-only UI but conflicts with existing language-switch requirements and loses structured details. + +4. Move routine refresh state into ViewModel-observable properties and command state. + + The current code-behind `Refresh()` updates clip selector state, combine menu checks, and command availability. These should be bound to `SelectedClipIndex`, `IsClipCombineChecked`, capability flags, and commands that raise availability changes when relevant state changes. + + Alternative considered: keep `Refresh()` and add more tests around it. That preserves current behavior but keeps state synchronization split across ViewModel and view, which is the same failure mode this change is trying to prevent. + +5. Keep code-behind as an adapter for platform UI events. + + Browse/save dialogs, drag/drop file extraction, DataGrid edit event adaptation, and responsive layout rearrangement are view concerns. The boundary issue is not the presence of code-behind; it is code-behind owning business or routine state synchronization. + +6. Preserve registry-based import resolution and fallback as the only runtime importer routing path. + + `RuntimeChapterImporterRegistry` can keep internal extension switches because it is the registry implementation. ViewModels and windows must call load services and must not branch on file extensions to new parser instances. + +## Risks / Trade-offs + +- [Risk] Log events raised on background threads can update UI-bound collections from the wrong dispatcher. -> Mitigation: the log view/model subscribes through an adapter that marshals collection mutation to the UI dispatcher; tests cover event delivery without requiring a desktop session. +- [Risk] Removing manual `Refresh()` can expose missing property notifications or command availability raises. -> Mitigation: convert in small steps and add ViewModel/headless tests for clip switching, combine toggles, save availability, and frame/advanced option changes. +- [Risk] Extending `IApplicationLogService` affects test fakes and composition. -> Mitigation: update fakes in one pass and keep the new member minimal. +- [Risk] Background importers may report progress from worker threads. -> Mitigation: ViewModel progress and collection updates remain the only observable UI mutations; progress callbacks either update scalar state safely through the command path or are marshaled before touching UI-bound properties. +- [Risk] Static source assertions would be tempting for boundary checks. -> Mitigation: use compiled references, public service seams, ViewModel construction tests, headless UI behavior, and dependency-level tests instead of reading `.cs` or `.axaml` as text. + +## Migration Plan + +1. Extend the log-service contract and update `ApplicationLogPanelProvider`, tests, and log window/view-model subscription behavior. +2. Add tests that prove history replay plus incremental log events work and that clear removes history without disabling future events. +3. Move main-window option state and command availability refreshes into ViewModel properties/commands where they are currently mirrored by code-behind. +4. Reduce `MainWindow.Refresh()` to view-only adaptation or remove it once bindings and command notifications cover the state. +5. Add async load/threading regression tests using deterministic fake importers that complete asynchronously and verify `Rows`, `ClipOptions`, progress, and status update through ViewModel command flow. +6. Validate focused Avalonia and Infrastructure tests, then run the full solution test command before archiving. + +## Open Questions + +- Should `IApplicationLogService` expose a single `EntryAdded` event, or a richer change event that also reports `Cleared`? The lean initial path is `EntryAdded` plus existing `Clear()`, with a richer event only if the log window needs clear notifications for live views. +- Should command-state notification be centralized inside `UiCommand` with observed dependencies, or manually raised from ViewModel setters? Manual raises match the current codebase and are lower risk; a dependency-aware command helper can be considered later if duplication grows. diff --git a/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/proposal.md b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/proposal.md new file mode 100644 index 0000000..ffaf644 --- /dev/null +++ b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/proposal.md @@ -0,0 +1,29 @@ +## Why + +`docs/todo-list.md` captures the architectural guardrails learned from the legacy implementation: UI logging must be observable and bounded, ViewModels must stay independent of `Window`, chapter loading must not update observable UI collections from background work, import routing must remain registry-based, and UI prompts must use explicit services. The current Avalonia rewrite largely follows these directions, but the contracts are not fully explicit and a few seams still depend on manual UI refreshes or snapshot-only services, which makes regressions easy as more legacy workflows are restored. + +## What Changes + +- Add an evented application-log contract so the log window can display history and receive newly captured log lines without polling or reconstructing the panel from snapshots. +- Keep the log buffer bounded and structured, including diagnostics from import, export, and external-tool fallback paths. +- Tighten the main-window boundary so `MainWindowViewModel` remains free of Avalonia `Window`/`StorageProvider` dependencies and routine UI state is driven by bindings and observable command state rather than manual code-behind refresh. +- Make the load pipeline contract explicit: background import work may perform IO/parsing, but observable UI collections are updated only by the ViewModel command flow on the UI thread. +- Preserve registry-based importer resolution and fallback, and document that adding formats must register importers instead of expanding ViewModel extension switches. +- Preserve explicit dialog/window/platform services for prompts and notifications; do not reintroduce static notification events or Core UI callbacks. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `supporting-ui-platform-services`: strengthen logging, dialog, window, and platform-service requirements around bounded evented logs and explicit UI notification services. +- `avalonia-ui-shell`: strengthen main-window requirements around ViewModel/window decoupling, binding-driven refresh, command state observability, and UI-thread collection updates after async loads. + +## Impact + +- Affected projects: `src/ChapterTool.Core`, `src/ChapterTool.Infrastructure`, `src/ChapterTool.Avalonia`. +- Affected tests: `tests/ChapterTool.Infrastructure.Tests` for log service behavior and platform services; `tests/ChapterTool.Avalonia.Tests` for ViewModel/window decoupling, command state, headless UI binding behavior, and async load/update behavior. +- No chapter file format, parser, exporter, or external-tool command-line compatibility changes are expected. diff --git a/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/specs/avalonia-ui-shell/spec.md b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/specs/avalonia-ui-shell/spec.md new file mode 100644 index 0000000..e450755 --- /dev/null +++ b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/specs/avalonia-ui-shell/spec.md @@ -0,0 +1,39 @@ +## ADDED Requirements + +### Requirement: Async load updates observable UI state safely +The Avalonia shell SHALL keep file IO and parsing separate from observable UI state mutation during asynchronous loads. + +#### Scenario: Import work completes asynchronously +- **WHEN** a load service or importer completes asynchronously after performing file IO or parsing +- **THEN** the main-window ViewModel SHALL update `Rows`, `ClipOptions`, selected clip state, status, and progress through its command flow +- **AND** background import code SHALL NOT mutate UI-bound `ObservableCollection` instances directly + +#### Scenario: Load progress does not bypass ViewModel state +- **WHEN** an importer reports intermediate progress during a load +- **THEN** progress updates SHALL be surfaced through ViewModel state or command-owned progress handling +- **AND** the view SHALL NOT rely on importer callbacks to update controls directly + +### Requirement: Main window ViewModel stays independent of Avalonia windows +The main-window ViewModel SHALL remain independent of Avalonia `Window`, storage-provider, and control instances. + +#### Scenario: File picking remains view or service responsibility +- **WHEN** a user browses for a source file, MPLS file, chapter-name template, or save directory +- **THEN** the selection SHALL be performed by a file-picker service or view adapter +- **AND** the ViewModel SHALL receive the selected path or cancellation result without holding an Avalonia `Window` + +#### Scenario: Routine state does not require manual window refresh +- **WHEN** clip selection, combine state, save availability, selected rows, frame options, naming options, expression options, or current chapter data changes +- **THEN** the ViewModel SHALL raise observable property or command-availability notifications sufficient for bound controls to update +- **AND** the window SHALL NOT need to run a routine manual refresh method to synchronize that state + +### Requirement: Import formats are routed through the importer registry +The Avalonia shell SHALL route source loading through the load service and importer registry rather than constructing importers from ViewModel or window extension switches. + +#### Scenario: New source format is added +- **WHEN** support for a new chapter source format is introduced +- **THEN** runtime selection SHALL be added through an `IChapterImporterRegistry` implementation or registered importer path +- **AND** `MainWindowViewModel` SHALL continue to call the load service without branching on the file extension + +#### Scenario: Import fallback diagnostics are logged structurally +- **WHEN** the primary importer cannot be invoked and a fallback importer is used +- **THEN** the load pipeline SHALL return or log a structured diagnostic identifying the primary importer, fallback importer, source path context, and reason for fallback diff --git a/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/specs/supporting-ui-platform-services/spec.md b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/specs/supporting-ui-platform-services/spec.md new file mode 100644 index 0000000..b885497 --- /dev/null +++ b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/specs/supporting-ui-platform-services/spec.md @@ -0,0 +1,32 @@ +## ADDED Requirements + +### Requirement: Application log service is evented and bounded +The application log service SHALL expose recent log history and notify subscribers when new user-visible log entries are captured, while retaining only a bounded in-memory set. + +#### Scenario: Log window receives existing history then new entries +- **WHEN** a log window or log view model is opened after log entries already exist +- **THEN** it SHALL be able to read the existing bounded log history +- **AND** when a subsequent log event passes the UI sink filter it SHALL receive a notification containing the new structured log entry + +#### Scenario: Log buffer remains bounded while events continue +- **WHEN** more log entries are captured than the configured UI log capacity +- **THEN** the log service SHALL retain only the most recent bounded entries +- **AND** it SHALL continue notifying subscribers for newly accepted entries + +#### Scenario: Clearing history does not disable live logging +- **WHEN** the user clears the log window history +- **THEN** the in-memory log history SHALL be empty +- **AND** subsequent captured log entries SHALL still be retained and delivered to subscribers + +### Requirement: UI notifications use explicit services +The application SHALL route user-facing prompts, tool windows, clipboard operations, shell operations, and platform integrations through explicit injectable services rather than static notification events or Core-to-UI callbacks. + +#### Scenario: ViewModels request UI interactions through services +- **WHEN** a ViewModel needs to show a prompt, open an auxiliary window, copy text, open related media, select files, or request a platform-gated action +- **THEN** it SHALL use the corresponding service abstraction or command parameter flow +- **AND** it SHALL NOT require a static notification event or a direct Avalonia `Window` reference + +#### Scenario: Core remains UI notification independent +- **WHEN** Core services import, edit, transform, convert, or export chapters +- **THEN** they SHALL return structured results or diagnostics to callers +- **AND** they SHALL NOT display UI prompts or publish static UI notification events diff --git a/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/tasks.md b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/tasks.md new file mode 100644 index 0000000..5c8396b --- /dev/null +++ b/openspec/changes/archive/2026-07-03-harden-ui-service-boundaries/tasks.md @@ -0,0 +1,36 @@ +## 1. Evented Application Log Service + +- [x] 1.1 Extend the UI log service contract with a structured new-entry notification while preserving snapshot history, formatting, and clear behavior. +- [x] 1.2 Update `ApplicationLogPanelProvider` to raise notifications only for entries accepted by the configured level filter and retained by the bounded buffer. +- [x] 1.3 Add or update Infrastructure tests for history replay, new-entry delivery, capacity trimming, severity filtering, structured state retention, and clear-then-continue behavior. +- [x] 1.4 Update log-window or tool-view behavior so live logs append from service notifications and existing logs still render when the window opens. +- [x] 1.5 Verify log entries continue to format through active localization resources and preserve technical details after language changes. + +## 2. Main-Window ViewModel and Service Boundaries + +- [x] 2.1 Audit `MainWindowViewModel` constructor and command paths to confirm file picking, windows, shell, clipboard, prompts, settings, and platform actions remain behind services or command parameters. +- [x] 2.2 Move routine state currently synchronized by `MainWindow.Refresh()` into observable ViewModel properties and command availability notifications. +- [x] 2.3 Bind combine checked state, clip selection, save availability, related-media availability, and auxiliary command enabled state directly to ViewModel state or commands. +- [x] 2.4 Keep code-behind only for view adaptation: file picker invocation, drag/drop path extraction, DataGrid edit event mapping, shortcut adaptation, and responsive layout adjustments. +- [x] 2.5 Add or update ViewModel and headless UI tests proving state changes update visible controls without calling a routine manual refresh method. + +## 3. Async Load and Import Routing + +- [x] 3.1 Add a deterministic async load test that completes after an await boundary and verifies `Rows`, `ClipOptions`, selected clip state, status text, and progress update through `MainWindowViewModel`. +- [x] 3.2 Ensure importer progress callbacks update ViewModel-owned progress/status state without direct control mutation. +- [x] 3.3 Add or update tests covering importer registry resolution for existing extensions and the fallback cases called out in the current specs. +- [x] 3.4 Verify `MainWindowViewModel` and `MainWindow` continue to call load services without constructing importers or branching on source extensions. +- [x] 3.5 Ensure fallback diagnostics for ffprobe, mkvextract, FLAC embedded CUE, and unsupported sources are returned or logged structurally. + +## 4. Prompt and Notification Boundaries + +- [x] 4.1 Verify Core services return structured results or diagnostics instead of displaying UI prompts or publishing static notification events. +- [x] 4.2 Verify ViewModels route user-facing prompts, tool windows, clipboard operations, related-media launch, file association, and unsupported-platform feedback through explicit services. +- [x] 4.3 Add focused tests for prompt/window service substitution where boundary behavior is not already covered. + +## 5. Validation + +- [x] 5.1 Run `openspec validate "harden-ui-service-boundaries" --strict`. +- [x] 5.2 Run `dotnet test tests/ChapterTool.Infrastructure.Tests/ChapterTool.Infrastructure.Tests.csproj --no-restore`. +- [x] 5.3 Run `dotnet test tests/ChapterTool.Avalonia.Tests/ChapterTool.Avalonia.Tests.csproj --no-restore`. +- [x] 5.4 Run `dotnet test ChapterTool.Avalonia.slnx --no-restore`. diff --git a/openspec/specs/avalonia-ui-shell/spec.md b/openspec/specs/avalonia-ui-shell/spec.md index ae89309..58dfd60 100644 --- a/openspec/specs/avalonia-ui-shell/spec.md +++ b/openspec/specs/avalonia-ui-shell/spec.md @@ -259,6 +259,44 @@ The settings panel SHALL organize the durable configurable features discovered f - **WHEN** the settings panel is opened - **THEN** it SHALL expose MKVToolNix/mkvextract path, eac3to path, ffprobe path, and ffmpeg directory fallback controls with browse, clear, and validation status behavior +### Requirement: Async load updates observable UI state safely +The Avalonia shell SHALL keep file IO and parsing separate from observable UI state mutation during asynchronous loads. + +#### Scenario: Import work completes asynchronously +- **WHEN** a load service or importer completes asynchronously after performing file IO or parsing +- **THEN** the main-window ViewModel SHALL update `Rows`, `ClipOptions`, selected clip state, status, and progress through its command flow +- **AND** background import code SHALL NOT mutate UI-bound `ObservableCollection` instances directly + +#### Scenario: Load progress does not bypass ViewModel state +- **WHEN** an importer reports intermediate progress during a load +- **THEN** progress updates SHALL be surfaced through ViewModel state or command-owned progress handling +- **AND** the view SHALL NOT rely on importer callbacks to update controls directly + +### Requirement: Main window ViewModel stays independent of Avalonia windows +The main-window ViewModel SHALL remain independent of Avalonia `Window`, storage-provider, and control instances. + +#### Scenario: File picking remains view or service responsibility +- **WHEN** a user browses for a source file, MPLS file, chapter-name template, or save directory +- **THEN** the selection SHALL be performed by a file-picker service or view adapter +- **AND** the ViewModel SHALL receive the selected path or cancellation result without holding an Avalonia `Window` + +#### Scenario: Routine state does not require manual window refresh +- **WHEN** clip selection, combine state, save availability, selected rows, frame options, naming options, expression options, or current chapter data changes +- **THEN** the ViewModel SHALL raise observable property or command-availability notifications sufficient for bound controls to update +- **AND** the window SHALL NOT need to run a routine manual refresh method to synchronize that state + +### Requirement: Import formats are routed through the importer registry +The Avalonia shell SHALL route source loading through the load service and importer registry rather than constructing importers from ViewModel or window extension switches. + +#### Scenario: New source format is added +- **WHEN** support for a new chapter source format is introduced +- **THEN** runtime selection SHALL be added through an `IChapterImporterRegistry` implementation or registered importer path +- **AND** `MainWindowViewModel` SHALL continue to call the load service without branching on the file extension + +#### Scenario: Import fallback diagnostics are logged structurally +- **WHEN** the primary importer cannot be invoked and a fallback importer is used +- **THEN** the load pipeline SHALL return or log a structured diagnostic identifying the primary importer, fallback importer, source path context, and reason for fallback + #### Scenario: Output defaults are editable - **WHEN** the settings panel is opened - **THEN** it SHALL expose default save format and default XML chapter language rather than the current working values being edited on the main screen diff --git a/openspec/specs/supporting-ui-platform-services/spec.md b/openspec/specs/supporting-ui-platform-services/spec.md index ccba334..73059cb 100644 --- a/openspec/specs/supporting-ui-platform-services/spec.md +++ b/openspec/specs/supporting-ui-platform-services/spec.md @@ -259,6 +259,37 @@ Settings-related UI behavior SHALL use injectable services for settings stores, - **WHEN** settings validation, save, reset, or unsupported-platform feedback is displayed - **THEN** visible messages SHALL be formatted through the active localization resources +### Requirement: Application log service is evented and bounded +The application log service SHALL expose recent log history and notify subscribers when new user-visible log entries are captured, while retaining only a bounded in-memory set. + +#### Scenario: Log window receives existing history then new entries +- **WHEN** a log window or log view model is opened after log entries already exist +- **THEN** it SHALL be able to read the existing bounded log history +- **AND** when a subsequent log event passes the UI sink filter it SHALL receive a notification containing the new structured log entry + +#### Scenario: Log buffer remains bounded while events continue +- **WHEN** more log entries are captured than the configured UI log capacity +- **THEN** the log service SHALL retain only the most recent bounded entries +- **AND** it SHALL continue notifying subscribers for newly accepted entries + +#### Scenario: Clearing history does not disable live logging +- **WHEN** the user clears the log window history +- **THEN** the in-memory log history SHALL be empty +- **AND** subsequent captured log entries SHALL still be retained and delivered to subscribers + +### Requirement: UI notifications use explicit services +The application SHALL route user-facing prompts, tool windows, clipboard operations, shell operations, and platform integrations through explicit injectable services rather than static notification events or Core-to-UI callbacks. + +#### Scenario: ViewModels request UI interactions through services +- **WHEN** a ViewModel needs to show a prompt, open an auxiliary window, copy text, open related media, select files, or request a platform-gated action +- **THEN** it SHALL use the corresponding service abstraction or command parameter flow +- **AND** it SHALL NOT require a static notification event or a direct Avalonia `Window` reference + +#### Scenario: Core remains UI notification independent +- **WHEN** Core services import, edit, transform, convert, or export chapters +- **THEN** they SHALL return structured results or diagnostics to callers +- **AND** they SHALL NOT display UI prompts or publish static UI notification events + ### Requirement: Application diagnostics use standard logging middleware The application SHALL route diagnostic logging through standard .NET logging abstractions backed by a structured logging provider instead of using a hand-written in-memory list as the primary logging mechanism. diff --git a/src/ChapterTool.Avalonia/ChapterTool.Avalonia.csproj b/src/ChapterTool.Avalonia/ChapterTool.Avalonia.csproj index 338f268..1828799 100644 --- a/src/ChapterTool.Avalonia/ChapterTool.Avalonia.csproj +++ b/src/ChapterTool.Avalonia/ChapterTool.Avalonia.csproj @@ -10,13 +10,13 @@ - - - - + + + + - - + + diff --git a/src/ChapterTool.Avalonia/Localization/Resources/Strings.en-US.resx b/src/ChapterTool.Avalonia/Localization/Resources/Strings.en-US.resx index 6d93d29..693e9b4 100644 --- a/src/ChapterTool.Avalonia/Localization/Resources/Strings.en-US.resx +++ b/src/ChapterTool.Avalonia/Localization/Resources/Strings.en-US.resx @@ -55,7 +55,7 @@ Auto frame-rate detection: option={option}, confidence={confidence}, accurate={accurate}/{evaluated}, deviation={deviation} - Change FPS: {sourceFps} -> {targetFps}, chapters {before} -> {after} + Convert to current FPS: source={sourceFps}, target={targetFps}, chapters {before} -> {after} Create zones: selectedRows={selectedRows}, chapters={chapters} @@ -115,7 +115,7 @@ Chapter name - Change FPS + Convert to current FPS Frames diff --git a/src/ChapterTool.Avalonia/Localization/Resources/Strings.ja-JP.resx b/src/ChapterTool.Avalonia/Localization/Resources/Strings.ja-JP.resx index 4b4ed57..93b30b2 100644 --- a/src/ChapterTool.Avalonia/Localization/Resources/Strings.ja-JP.resx +++ b/src/ChapterTool.Avalonia/Localization/Resources/Strings.ja-JP.resx @@ -55,7 +55,7 @@ 自動フレームレート検出: option={option}, confidence={confidence}, accurate={accurate}/{evaluated}, deviation={deviation} - FPS を変換: {sourceFps} -> {targetFps}, chapters {before} -> {after} + 現在の FPS に変換: source={sourceFps}, target={targetFps}, chapters {before} -> {after} Zones 生成: selectedRows={selectedRows}, chapters={chapters} @@ -115,7 +115,7 @@ チャプター名 - FPS を変換 + 現在の FPS に変換 フレーム diff --git a/src/ChapterTool.Avalonia/Localization/Resources/Strings.zh-CN.resx b/src/ChapterTool.Avalonia/Localization/Resources/Strings.zh-CN.resx index 77e6864..ccffee1 100644 --- a/src/ChapterTool.Avalonia/Localization/Resources/Strings.zh-CN.resx +++ b/src/ChapterTool.Avalonia/Localization/Resources/Strings.zh-CN.resx @@ -55,7 +55,7 @@ 自动帧率检测:option={option},confidence={confidence},accurate={accurate}/{evaluated},deviation={deviation} - 转换帧率:{sourceFps} -> {targetFps},章节数 {before} -> {after} + 转换至当前帧率:源配置={sourceFps},目标配置={targetFps},章节数 {before} -> {after} 生成 Zones:选中行={selectedRows},章节数={chapters} @@ -115,7 +115,7 @@ 章节名 - 转换帧率 + 转换至当前帧率 帧数 diff --git a/src/ChapterTool.Avalonia/Services/AvaloniaWindowService.cs b/src/ChapterTool.Avalonia/Services/AvaloniaWindowService.cs index 427364f..c7041ee 100644 --- a/src/ChapterTool.Avalonia/Services/AvaloniaWindowService.cs +++ b/src/ChapterTool.Avalonia/Services/AvaloniaWindowService.cs @@ -146,7 +146,11 @@ private Control CreateContent(Window window, string id, MainWindowViewModel view { DataContext = new TextToolViewModel( viewModel.LogText, - new TextToolOptions { ClearAction = viewModel.ClearLog }) + new TextToolOptions + { + ClearAction = viewModel.ClearLog, + LiveRefreshService = viewModel.LogService + }) }, "settings" => new SettingsToolView { diff --git a/src/ChapterTool.Avalonia/Services/RuntimeChapterLoadService.cs b/src/ChapterTool.Avalonia/Services/RuntimeChapterLoadService.cs index 74d95d1..39a36c3 100644 --- a/src/ChapterTool.Avalonia/Services/RuntimeChapterLoadService.cs +++ b/src/ChapterTool.Avalonia/Services/RuntimeChapterLoadService.cs @@ -44,10 +44,13 @@ private async ValueTask LoadWithFallbackAsync( } var fallbackResult = await fallback.ImportAsync(new ChapterImportRequest(path, Progress: progress), cancellationToken); + var fallbackReason = primaryResult.Diagnostics.FirstOrDefault(); var fallbackDiagnostic = new ChapterDiagnostic( DiagnosticSeverity.Info, "ImporterFallbackUsed", - $"Primary importer '{importer.Id}' could not be invoked; used fallback importer '{fallback.Id}'."); + $"Primary importer '{importer.Id}' could not be invoked; used fallback importer '{fallback.Id}'.", + path, + $"primary={importer.Id}; fallback={fallback.Id}; reason={fallbackReason?.Code ?? "Unknown"}"); var diagnostics = primaryResult.Diagnostics.Concat([fallbackDiagnostic]).Concat(fallbackResult.Diagnostics).ToList(); return fallbackResult with { Diagnostics = diagnostics }; } diff --git a/src/ChapterTool.Avalonia/ViewModels/MainWindowViewModel.cs b/src/ChapterTool.Avalonia/ViewModels/MainWindowViewModel.cs index e6b88fd..612354c 100644 --- a/src/ChapterTool.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/src/ChapterTool.Avalonia/ViewModels/MainWindowViewModel.cs @@ -32,6 +32,7 @@ public sealed class MainWindowViewModel : ObservableViewModel private ChapterInfoGroup? currentGroup; private ChapterInfo? currentInfo; private FrameRateOption selectedFrameRateOption; + private decimal? configuredFrameRate; private bool currentInfoBelongsToSelectedClip; private ChapterInfoGroup? splitClipGroup; private ChapterSourceOption? combinedClipOption; @@ -184,6 +185,23 @@ public int SelectedClipIndex if (SetProperty(ref field, value)) { OnPropertyChanged(nameof(RelatedMediaReferences)); + OnPropertyChanged(nameof(SelectedClipDisplayOption)); + } + } + } + + public SelectorDisplayOption? SelectedClipDisplayOption + { + get => SelectedClipIndex < 0 || SelectedClipIndex >= ClipDisplayOptions.Count + ? null + : ClipDisplayOptions[SelectedClipIndex]; + set + { + var index = value is null ? -1 : ClipDisplayOptions.IndexOf(value); + if (index >= 0 && index != SelectedClipIndex) + { + SelectClip(index); + NotifyStateChanged(); } } } @@ -591,6 +609,8 @@ public string BuildPreview() public string LogText() => logService.Format(FormatLogEntry); + public IApplicationLogService LogService => logService; + public void ClearLog() => logService.Clear(); public void UpdateSelectedRows(IReadOnlySet indexes) @@ -737,6 +757,7 @@ private void SelectClip(int index) SelectedClipIndex = index; currentInfo = ClipOptions[index].ChapterInfo; + configuredFrameRate = (decimal)currentInfo.FramesPerSecond; currentInfoBelongsToSelectedClip = !IsClipCombineChecked; Log("Log.SelectedSourceOption", ("index", index), @@ -901,6 +922,9 @@ private void ApplyFrameInfo() var result = frameRateService.UpdateFrames(currentInfo, appliedOption, RoundFrames, FrameAccuracyTolerance); currentInfo = result.Info; + var storedInfo = configuredFrameRate is null + ? currentInfo + : currentInfo with { FramesPerSecond = (double)configuredFrameRate.Value }; if (detection is not null) { @@ -926,11 +950,11 @@ private void ApplyFrameInfo() ("chapters", currentInfo.Chapters.Count)); if (currentInfoBelongsToSelectedClip) { - UpdateCurrentClipOption(currentInfo); + UpdateCurrentClipOption(storedInfo); } else if (IsClipCombineChecked) { - UpdateCombinedClipOption(currentInfo); + UpdateCombinedClipOption(storedInfo); } RefreshRows(); NotifyStateChanged(); @@ -943,8 +967,10 @@ private void ChangeFpsToSelectedOption() return; } - var sourceFps = (decimal)currentInfo.FramesPerSecond; - var result = ChapterFpsTransformService.ChangeFps(currentInfo, sourceFps, selectedFrameRateOption.Value); + var sourceFps = configuredFrameRate ?? (decimal)currentInfo.FramesPerSecond; + var targetOption = selectedFrameRateOption; + var targetFps = targetOption.Value; + var result = ChapterFpsTransformService.ChangeFps(currentInfo, sourceFps, targetFps); if (!result.Success) { SetStatus(null, diagnostic: result.Diagnostics.FirstOrDefault()); @@ -955,10 +981,12 @@ private void ChangeFpsToSelectedOption() var beforeCount = currentInfo.Chapters.Count; currentInfo = result.Info; + configuredFrameRate = targetFps; ApplyFrameInfo(); SetStatus("Status.Updated"); - Log("Log.EditChapters", - ("action", $"Change FPS: {sourceFps:0.###} -> {selectedFrameRateOption.Value:0.###}"), + Log("Log.ChangeFps", + ("sourceFps", $"{sourceFps:0.###}"), + ("targetFps", $"{targetFps:0.###}"), ("before", beforeCount), ("after", result.Info.Chapters.Count)); LogStatus(); @@ -1097,6 +1125,8 @@ private void OnClipOptionsChanged(object? sender, NotifyCollectionChangedEventAr SyncClipDisplayOptions(args); OnPropertyChanged(nameof(IsClipSelectionVisible)); OnPropertyChanged(nameof(RelatedMediaReferences)); + OnPropertyChanged(nameof(SelectedClipIndex)); + OnPropertyChanged(nameof(SelectedClipDisplayOption)); NotifyCommandStates(); } diff --git a/src/ChapterTool.Avalonia/ViewModels/ToolWindowViewModels.cs b/src/ChapterTool.Avalonia/ViewModels/ToolWindowViewModels.cs index 4847ed3..325b5d7 100644 --- a/src/ChapterTool.Avalonia/ViewModels/ToolWindowViewModels.cs +++ b/src/ChapterTool.Avalonia/ViewModels/ToolWindowViewModels.cs @@ -38,6 +38,11 @@ public TextToolViewModel(Func refreshText, TextToolOptions? options = nu Text = string.Empty; return ValueTask.CompletedTask; }, _ => this.options.ClearAction is not null); + + if (this.options.LiveRefreshService is { } liveRefreshService) + { + liveRefreshService.EntryAdded += OnEntryAdded; + } } public string Text @@ -99,6 +104,16 @@ public IReadOnlyList Lines public UiCommand ClearCommand { get; } + private void OnEntryAdded(object? sender, ApplicationLogEntry entry) + { + RefreshText(); + } + + private void RefreshText() + { + Text = Format(refreshText(), Kind); + } + private static string Format(string text, TextToolKind kind) { if (string.IsNullOrWhiteSpace(text)) @@ -283,6 +298,8 @@ public sealed class TextToolOptions public Action? ClearAction { get; init; } public TextToolFormatSelector? FormatSelector { get; init; } + + public IApplicationLogService? LiveRefreshService { get; init; } } public sealed class TextToolFormatSelector(MainWindowViewModel owner) diff --git a/src/ChapterTool.Avalonia/Views/MainWindow.axaml b/src/ChapterTool.Avalonia/Views/MainWindow.axaml index 9bfca1b..f8baa6a 100644 --- a/src/ChapterTool.Avalonia/Views/MainWindow.axaml +++ b/src/ChapterTool.Avalonia/Views/MainWindow.axaml @@ -59,6 +59,23 @@ + + + + + + + + + + @@ -242,6 +261,27 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/ChapterTool.Avalonia.Tests/Headless/ToolViewsHeadlessTests.cs b/tests/ChapterTool.Avalonia.Tests/Headless/ToolViewsHeadlessTests.cs index ed7d5bd..a8a5133 100644 --- a/tests/ChapterTool.Avalonia.Tests/Headless/ToolViewsHeadlessTests.cs +++ b/tests/ChapterTool.Avalonia.Tests/Headless/ToolViewsHeadlessTests.cs @@ -268,6 +268,7 @@ public async Task Settings_tool_renders_grouped_configuration_controls() host.ThemeSettingsStore, host.Localizer, host.SettingsPickerService, + shellService: host.ShellService, autoLoad: false); await viewModel.LoadAsync(TestContext.Current.CancellationToken); @@ -279,9 +280,23 @@ public async Task Settings_tool_renders_grouped_configuration_controls() Assert.True(MainWindowHeadlessTestHost.ContainsRenderedTextStatic(window, "Output Defaults")); Assert.True(MainWindowHeadlessTestHost.ContainsRenderedTextStatic(window, "Appearance")); + Assert.True(MainWindowHeadlessTestHost.ContainsRenderedTextStatic(window, "About")); Assert.Contains(MainWindowHeadlessTestHost.Descendants(window), textBox => textBox.Text == viewModel.SaveDirectory); Assert.Contains(MainWindowHeadlessTestHost.Descendants(window), comboBox => Equals(comboBox.ItemsSource, viewModel.Languages)); Assert.Contains(MainWindowHeadlessTestHost.Descendants - @@ -122,10 +122,10 @@ - - @@ -136,10 +136,10 @@ - - @@ -150,10 +150,10 @@ - - @@ -164,10 +164,10 @@ - - diff --git a/tests/ChapterTool.Avalonia.Tests/Headless/MainWindowInteractionHeadlessTests.cs b/tests/ChapterTool.Avalonia.Tests/Headless/MainWindowInteractionHeadlessTests.cs index fa9076c..9ab93d7 100644 --- a/tests/ChapterTool.Avalonia.Tests/Headless/MainWindowInteractionHeadlessTests.cs +++ b/tests/ChapterTool.Avalonia.Tests/Headless/MainWindowInteractionHeadlessTests.cs @@ -1,3 +1,4 @@ +using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Headless.XUnit; using Avalonia.Input; @@ -142,4 +143,16 @@ public async Task Auxiliary_command_surfaces_are_visible_and_bound() Assert.Equal(["preview"], host.WindowService.Opened); Assert.Same(host.ViewModel, host.WindowService.Parameters.Single()); } + + [AvaloniaFact] + public async Task Icon_only_main_window_buttons_have_accessible_names() + { + using var host = new MainWindowHeadlessTestHost(); + await host.LayoutAsync(); + + Assert.Equal("Preview", AutomationProperties.GetName(host.RequiredControl