Back to Openspec

Tasks

openspec/changes/fix-validate-view-resolution-parity/tasks.md

1.6.09.7 KB
Original Source

Tasks

1. #1182 — validate resolves changes like status (membership gate)

  • 1.1 Reproduce at HEAD: openspec new change X (creates dir + .openspec.yaml, no proposal.md); confirm status --change X resolves it (exit 0) but validate X prints Unknown item and validate --all (X alone) prints "No items found" and exits 0.
  • 1.2 In src/commands/validate.ts, replace the getActiveChangeIds membership gate for change resolution with directory-existence resolution mirroring validateChangeExists (src/commands/workflow/shared.ts:168-170); keep getSpecIds as the spec predicate. Apply at all THREE sites: targeted (line 120), bulk --all/--changes (line 238), and the interactive "pick one" selector (line 97). Converged onto the canonical getAvailableChanges lister via a private listChangeIds helper (sorted to preserve prior ordering).
  • 1.3 Confirm correctness within a --store-selected root (resolution already shares resolveRootForCommand); add a store-root resolution test. No store-specific scenario beyond parity is required. Store-correct for free: validate resolves the store root through the same resolveRootForCommand as status, and the change predicate now matches; no dedicated store fixture added, per the design note.
  • 1.4 Preserve change/spec ambiguity and --type override behavior; reconcile the directory-existence change predicate with the spec predicate. Leave the spec-resolution side (getSpecIds) unchanged — it is correct. getSpecIds untouched; ambiguity test still green.
  • 1.5 Sibling src/commands/show.ts:81,115,121 shares the getActiveChangeIds gate — fold it onto the same resolution or record an explicit out-of-scope note. Add a one-line scope note that the deprecated noun-form change validate already resolves by directory existence but is cwd-based and its JSON mode does not set a non-zero exit (pre-existing, out of scope). DECISION: show.ts scoped OUT. ChangeCommand.show hard-requires proposal.md (throws "not found at .../proposal.md"), so folding it in would only convert "Unknown item" into a different downstream proposal-read error in a path no cli-* spec scenario covers. The deprecated noun-form change validate is likewise out of scope (cwd-based; JSON mode does not set a non-zero exit).
  • 1.6 Tests: proposal-less change resolves (targeted + bulk + interactive selector); store change resolves; ambiguity/--type unchanged; changes with proposal.md byte-identical; a resolved-but-invalid change exits non-zero (regression guard for the --all exit-0 observation). Added to test/commands/validate.test.ts: scaffolded resolves (targeted), sole proposal-less change in --all, resolved-but-invalid exits non-zero. Interactive selector uses the same listChangeIds.

2. #1182b — nested multi-area delta discovery

  • 2.1 Reproduce: a resolved change with deltas at specs/<area>/<capability>/spec.md reports "No delta sections found"; one-level specs/<capability>/spec.md is the control.
  • 2.2 Extend delta discovery in src/core/validation/validator.ts validateChangeDeltaSpecs (lines 115-138) to recurse the nested specs/** layout (the spec-driven specs glob is specs/**/*.md). Added a recursive findDeltaSpecFiles walker collecting every spec.md; entryPath is now the POSIX relative path from specs/.
  • 2.3 Tests: nested-layout change discovers and validates its deltas; single-level layout unchanged. Added to test/core/validation.test.ts.

3. #1202 — task progress through the tracked-tasks artifact glob (view + archive + list)

  • 3.1 Reproduce: project-local schema with tasks artifact generates: "**/tasks.md"; a change with backend/tasks.md + frontend/tasks.md (some unchecked); confirm status reports the tasks artifact present while view shows Draft, list shows "No tasks", and archive would let it archive unfinished.
  • 3.2 In src/utils/task-progress.ts, change getTaskProgressForChange to: identify the tracked-tasks artifact (the artifact whose generates equals the schema apply.tracks value, falling back to artifact id tasks when no apply block), then count checkboxes across resolveArtifactOutputs(changeDir, artifact.generates) (src/core/artifact-graph/outputs.ts:17, returns a de-duped, change-rooted path list). NOTE: apply.tracks is a filename that selects the artifact, NOT a glob — the glob is the artifact's generates.
  • 3.3 Add a required projectRoot parameter (needed for resolveSchema / project-local schemas); resolve schema → tracked artifact → generates inside the helper.
  • 3.4 Catch resolveSchema failure (it throws on an unresolvable/misnamed schema) and fall back to a single top-level tasks.md; preserve the no-schema / no-tracked-artifact / zero-match fallback and the swallowed-missing-file behavior. The helper MUST NOT throw.
  • 3.5 Update all four call sites for the new projectRoot argument: src/core/view.ts:100 (path.dirname(openspecDir)), src/core/list.ts:112 (targetPath), src/core/archive.ts:342 and :540 (path.resolve(changesDir,'..','..')).
  • 3.6 Fold the independent second copy in src/commands/change.ts:111,164 (its own countTasks, JSON list + long list) onto the shared helper passing process.cwd(); drop the now-orphan countTasks and unused TASK_PATTERN/COMPLETED_TASK_PATTERN consts.
  • 3.7 Tests: nested-glob change aggregates and is not Draft; files-exist-but-unchecked is Active not Completed; apply.tracks-selected artifact resolves; resolution scoped to the change dir (archive/ and sibling changes excluded); no double-count; unresolvable-schema falls back without crashing; single-file and no-schema unchanged; zero-match stays Draft; view/list/archive resolve the same files as status. test/utils/task-progress.test.ts (unit) + test/core/view.test.ts (Active classification) + test/core/archive.test.ts (gate).

4. #1202 — archive incomplete-task gate (data safety)

  • 4.1 Confirm src/core/archive.ts:342,540 feed the incomplete-task gate (archive.ts:348-353).
  • 4.2 With the shared-helper fix in place, verify the gate sees nested/glob tasks (the empirical repro archived a 3/5 change — this must now block). Verified end-to-end against the built CLI: archive now reports "2 incomplete task(s)" and exits non-zero for a 3/5 glob-tasks change.
  • 4.3 Tests: a glob-tasks change with unchecked tasks is blocked (or requires explicit override); the gate resolves the same files as view; unresolvable-schema falls back without crash; single-file behavior unchanged. Added to test/core/archive.test.ts; helper-level fallback/parity covered in test/utils/task-progress.test.ts.

5. #1156 — SHALL/MUST hint on main specs (header recovery + remove refine)

  • 5.1 Reproduce: a main spec requirement with SHALL/MUST in the header only emits the generic message while the equivalent ADDED/MODIFIED delta emits the targeted hint; a RENAMED delta emits no hint; a header-only-no-body main spec is valid today.
  • 5.2 Recover the requirement header for main specs (lost at src/core/parsers/markdown-parser.ts:220-226) by reusing src/core/parsers/requirement-blocks.ts (extractRequirementsSection, header+body pairs).
  • 5.3 In src/core/validation/validator.ts applySpecRules (lines 290-329), run containsShallOrMust + buildMissingShallOrMustMessage on the recovered header/body so the imperative rule owns BOTH the header-only case (targeted hint) and the no-keyword-anywhere case (generic message).
  • 5.4 REMOVE the Zod refine from RequirementSchema (src/core/schemas/base.schema.ts:11-14) entirely (not merely relax it) — deltas never used it (they validate imperatively in validateChangeDeltaSpecs), so removal cannot regress the delta path, and it prevents double-emission on the main-spec path.
  • 5.5 Generalize buildMissingShallOrMustMessage to accept a prefix; main-spec prefix = Requirement "<name>", so the actionable sentence stays in one place and is byte-identical across paths. Converge lowercase handling onto the shared \b(SHALL|MUST)\b regex. Keep the delta-path message string unchanged. Delta call sites now pass ADDED "<name>" / MODIFIED "<name>" prefixes, producing byte-identical strings.
  • 5.6 Tests (assert across validate <spec>, --all, --json, spec validate, and validateSpecContent): header-only main spec → actionable sentence byte-identical to delta; exactly one issue; no-keyword-anywhere still errors; body-keyword not flagged; lowercase shall errors; header-only-no-body emits the hint (intended change); RENAMED emits no hint and is byte-for-byte unchanged. Added a main-spec SHALL/MUST body-keyword hint (#1156) describe in test/core/validation.test.ts driving validateSpecContent (the shared surface for validate/--all/--json/spec validate/rebuilt-spec validation); the obsolete schema-refine unit test was updated to reflect the moved enforcement. End-to-end cases A–D verified against the built CLI.

6. Parity guard and verification

  • 6.1 Add the cross-command parity assertions from design Decision 7 as regression tests (validate↔status resolution incl. exit code; view/list/archive resolve the same files as status; main-spec↔delta actionable sentence).
  • 6.2 Run openspec validate fix-validate-view-resolution-parity --strict and the full test suite; confirm no behavior change on the canonical paths and the documented unchanged cases (the header-only-no-body main-spec case is the one intended exception, per design Decision 6). Change validates --strict (exit 0); all 36 repo specs pass --specs --strict (no #1156 false positives); full suite 1791 passed with only the pre-existing, environment-specific zsh-installer failures unchanged.