feat(validate): add --archived to lint task completion of archived changes - #1604
feat(validate): add --archived to lint task completion of archived changes#1604clay-good wants to merge 4 commits into
Conversation
…anges `openspec validate --archived` scans every change under changes/archive/ and fails (exit 1) if any has unchecked tasks in tasks.md. This catches changes archived with unfinished work — which the normal validate flow never sees, since it only looks at active changes — and is meant for a pre-commit or CI hook. It is a standalone, opt-in scope: it returns before any existing bulk path, so no current `validate` invocation changes behavior, and it does not re-validate already-applied spec deltas. Reuses getTaskProgressForChange (the same counter status/list/archive use) so task counting never forks, and reads root.archiveDir so it is store-aware. Closes #205 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe ChangesArchived task validation
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant ValidateCommand
participant ArchiveDirectory
participant TaskProgress
participant Output
CLI->>ValidateCommand: execute with archived=true
ValidateCommand->>ArchiveDirectory: discover archived change IDs
ValidateCommand->>TaskProgress: evaluate archived tasks
TaskProgress-->>ValidateCommand: completion and unreadable-file details
ValidateCommand->>Output: emit validation summary
ValidateCommand-->>CLI: set exit status
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying openspec-docs with
|
| Latest commit: |
6a1ca84
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6ee59831.openspec-docs.pages.dev |
| Branch Preview URL: | https://claude-openspec-backlog-tria-w0o2.openspec-docs.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/commands/validate.ts`:
- Around line 447-458: Update getTaskProgressForChange and the validation flow
around it so task-file read failures are exposed and converted into an ERROR
issue instead of being treated as complete. Preserve the documented successful
0/0 result only when no task files exist, while retaining the existing
incomplete-task reporting for successfully read files.
- Around line 411-420: Update listArchivedChangeIds to return an empty list only
when fs.readdir fails with an ENOENT error; propagate permission, I/O, and
invalid-path errors instead of treating them as an empty archive. Add a
regression test covering an existing archive path that is not a directory.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 2b07756e-1290-4c1a-a738-0a88b969f13b
📒 Files selected for processing (6)
.changeset/validate-archived-tasks.mddocs/cli.mdsrc/cli/index.tssrc/commands/validate.tssrc/core/completions/command-registry.tstest/cli-e2e/validate-archived-tasks.test.ts
… files Address adversarial + CodeRabbit review of `validate --archived`: - listArchivedChangeIds now returns [] only for ENOENT (missing archive dir) and rethrows permission/I/O/ENOTDIR errors, so a real archive-read failure exits 1 instead of silently reading as "no archived changes". - Add getTaskProgressDetailForChange, which reports task files that exist but cannot be read; --archived turns those into an ERROR (naming the file) rather than silently counting them as zero tasks. The shared getTaskProgressForChange now wraps it and drops the detail, so status/list/archive totals are byte-identical. - Start the spinner after listing so a thrown listing error never leaves a spinner running. Adds regression tests (archive path is a file; archived tasks.md is unreadable) and unit tests for the new detail variant. Docs: align the --archived table verb and add a troubleshooting one-liner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/cli-e2e/validate-archived-tasks.test.ts`:
- Around line 129-132: Update the path construction in the archived-task fixture
to pass “openspec”, “changes”, “archive”, “unreadable-change”, and “tasks.md” as
separate arguments to path.join, preserving the same filesystem location with
platform-native separators.
In `@test/utils/task-progress.test.ts`:
- Around line 338-347: Update the unreadable-file assertions in
getTaskProgressDetailForChange’s test to verify the reported path, not just
unreadable count. Compare the actual path with the created bad/tasks.md path
after canonicalizing both via fs.realpathSync.native().
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 8909cf11-d5e6-4431-8c59-4bd3ef0dfaa2
📒 Files selected for processing (6)
docs/cli.mddocs/troubleshooting.mdsrc/commands/validate.tssrc/utils/task-progress.tstest/cli-e2e/validate-archived-tasks.test.tstest/utils/task-progress.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/cli.md
- src/commands/validate.ts
- Build the unreadable-fixture path from separate path.join components instead of a hard-coded Unix-separator string. - Assert the reported unreadable path (canonicalized with realpathSync.native), not just the count, so a wrong path can't pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From three fresh adversarial reviews (scale/perf, flag/output-shape, filesystem/security): - perf: memoize schema→glob resolution across archived changes via a run-scoped SchemaGlobCache, so the same schema.yaml isn't re-parsed once per change (the archive is append-only and can hold thousands). Threaded as an optional arg; existing callers are unchanged. Loop stays sequential by design (per-change work is synchronous) — now documented. - output shape: issue `path` now follows validate's convention — 'tasks.md' for incomplete tasks, and the POSIX root-relative file path for an unreadable file (one issue per file) instead of the bare 'tasks'. - plain output: print `change/<id>` (matching the JSON `type` and bulk validation) instead of `archived/<id>`. - docs: correct the "Never throws" docstrings (glob resolution can throw on a malformed/unsafe schema; the caller guards it) and note the load-bearing projectRoot override for the archive path depth. Store-mode resolution confirmed correct by review. Tests updated + a memo regression test added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/task-progress.ts (1)
106-110: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winNormalize schema names before caching.
resolveSchemainsrc/core/artifact-graph/resolver.tsaccepts schema names with or without.yamlor.yml. This cache uses the raw name, so equivalent names create separate entries and re-parse the same schema. Normalize the key with the same rule asresolveSchema, or share a common normalizer.Proposed fix
const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot); - if (schemaGlobCache?.has(schemaName)) return schemaGlobCache.get(schemaName); + const cacheKey = schemaName.replace(/\.ya?ml$/, ''); + if (schemaGlobCache?.has(cacheKey)) return schemaGlobCache.get(cacheKey); const schema = resolveSchema(schemaName, projectRoot); const generates = findTrackedTasksArtifact(schema)?.generates; - schemaGlobCache?.set(schemaName, generates); + schemaGlobCache?.set(cacheKey, generates);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/task-progress.ts` around lines 106 - 110, Normalize schemaName using the same rule as resolveSchema before checking or updating schemaGlobCache, so names with and without .yaml or .yml share one cache entry. Update the cache access in the surrounding task-progress flow and reuse a shared normalizer if one already exists rather than introducing divergent normalization logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/utils/task-progress.ts`:
- Around line 106-110: Normalize schemaName using the same rule as resolveSchema
before checking or updating schemaGlobCache, so names with and without .yaml or
.yml share one cache entry. Update the cache access in the surrounding
task-progress flow and reuse a shared normalizer if one already exists rather
than introducing divergent normalization logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3a6e658c-bcbf-47d4-8041-9d29e87f66cb
📒 Files selected for processing (4)
src/commands/validate.tssrc/utils/task-progress.tstest/cli-e2e/validate-archived-tasks.test.tstest/utils/task-progress.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- test/utils/task-progress.test.ts
- src/commands/validate.ts
alfred-openspec
left a comment
There was a problem hiding this comment.
Reviewed exact head 581b780. The archived scope fails closed on archive and task read errors, respects the resolved Store and each schema's tracked task files, and leaves existing validation modes unchanged. Local build, lint, 281 focused tests, and the hosted Linux, macOS, Windows, security, CodeQL, and docs checks pass. The cache-key normalization comment is only an optional efficiency cleanup.
Status
LGTM — ready for review. New behavior is fully opt-in; no existing
validateinvocation changes.What was missing
openspec validateonly ever looks at active changes and specs — active-change discovery explicitly skipschanges/archive/. So there was no way to check that changes were finished when they were archived. A change can be archived with unchecked tasks (e.g. viaarchive --yespast the incomplete-task warning), and nothing surfaces it afterward. #205 asks for exactly this: a lint that fails in a pre-commit hook when archived changes still have open tasks.What it does
Adds an opt-in
--archivedflag toopenspec validate:# Fail if any archived change still has unchecked tasks openspec validate --archivedchanges/archive/and checks itstasks.mdcheckboxes.--jsonwith the same shape asvalidate --all.archivepath that is a file) throws and exits 1 rather than being silently treated as "no archived changes", which would let a pre-commit lint pass without inspecting anything.tasks.mdthat exists but cannot be read is reported as an ERROR (naming the file) and fails, instead of being silently counted as zero tasks.It is a standalone scope: the
--archivedbranch returns before any existing bulk path, sovalidate,--all,--changes,--specs, and<item>are byte-for-byte unchanged. It intentionally does not re-validate spec deltas of archived changes (their operations were already applied at archive time) — it only checks task completion, which is the concrete ask in #205.Why it's safe
options.archived; no default behavior changes.getTaskProgressDetailForChange— the same task counterstatus,list, andarchiveuse (now wrapped bygetTaskProgressForChange, which drops the extra "unreadable" detail so those three callers get byte-identical totals). "What counts as a task" never forks;--archivedonly additionally surfaces unreadable files.root.archiveDir, so it respects a selected--storelike the rest of the command.Proof it works
test/cli-e2e/validate-archived-tasks.test.ts(5 cases):archivepath is a file → exit 1 (not a silent pass);tasks.mdunreadable → exit 1 with a "could not read" error.Plus unit tests in
test/utils/task-progress.test.tsfor the new detail variant (normal / unreadable / missing).Full suite green locally (3867 passing; two unrelated failures —
artifact-workflow"creates skills for Cursor tool" andconfig-profile"confirmed project apply…" — are pre-existing onmainand pass in CI).Notes / scope
--archivedis a standalone scope and does not compose with--all/--strict; a "lint everything including archives" gate is two commands:openspec validate --all --strict && openspec validate --archived. Kept standalone deliberately to keep the change surgical and side-effect-free.validate: rows printchange/<id>(mapping to the JSONtype), issuepathvalues follow thetasks.md/ root-relative-file convention, and schema resolution is memoized across archived changes so a large archive isn't re-parsed per change.docs/cli.md, regenerated website reference, and adocs/troubleshooting.mdone-liner) and aminorchangeset added.Closes #205
Summary by CodeRabbit
New Features
openspec validate --archivedcommand.Documentation
Tests