@nklisch/pi-agile-workflow 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +28 -0
  3. package/CHANGELOG.md +591 -0
  4. package/README.md +107 -0
  5. package/docs/ARCHITECTURE.md +716 -0
  6. package/docs/MIGRATION.md +419 -0
  7. package/docs/PRINCIPLES.md +464 -0
  8. package/docs/ROADMAP.md +95 -0
  9. package/docs/SPEC.md +855 -0
  10. package/docs/VISION.md +106 -0
  11. package/extensions/agile-workflow.test.ts +419 -0
  12. package/extensions/agile-workflow.ts +467 -0
  13. package/hooks/hooks.json +52 -0
  14. package/hooks/scripts/prompt-context.py +628 -0
  15. package/hooks/scripts/substrate-maintainer.py +359 -0
  16. package/hooks/scripts/test_prompt_context.py +757 -0
  17. package/package.json +30 -0
  18. package/scripts/install-work-view.sh +165 -0
  19. package/scripts/tests/agent-metadata.test.sh +75 -0
  20. package/scripts/tests/bump-version.test.sh +552 -0
  21. package/scripts/tests/channel-parity.test.sh +117 -0
  22. package/scripts/tests/convert-content-integrity.test.sh +246 -0
  23. package/scripts/tests/convert-install-routing.test.sh +159 -0
  24. package/scripts/tests/install-work-view.test.sh +411 -0
  25. package/scripts/tests/pi-package-metadata.test.sh +119 -0
  26. package/scripts/tests/work-board-shim.test.sh +239 -0
  27. package/scripts/tests/work-view-dist-version.test.sh +105 -0
  28. package/scripts/work-board.sh +64 -0
  29. package/scripts/work-view.sh +423 -0
  30. package/skills/autopilot/SKILL.md +330 -0
  31. package/skills/autopilot/agents/openai.yaml +6 -0
  32. package/skills/board/SKILL.md +62 -0
  33. package/skills/board/agents/openai.yaml +6 -0
  34. package/skills/bold-refactor/SKILL.md +244 -0
  35. package/skills/bold-refactor/agents/openai.yaml +6 -0
  36. package/skills/bug-scan/SKILL.md +378 -0
  37. package/skills/bug-scan/agents/openai.yaml +6 -0
  38. package/skills/bug-scan/references/async-promises.md +119 -0
  39. package/skills/bug-scan/references/concurrency-races.md +148 -0
  40. package/skills/bug-scan/references/data-layer.md +167 -0
  41. package/skills/bug-scan/references/error-handling.md +197 -0
  42. package/skills/bug-scan/references/gate-item-template.md +94 -0
  43. package/skills/bug-scan/references/language-footguns.md +219 -0
  44. package/skills/bug-scan/references/parked-item-template.md +103 -0
  45. package/skills/bug-scan/references/report-template.md +115 -0
  46. package/skills/bug-scan/references/resource-leaks.md +199 -0
  47. package/skills/bug-scan/references/state-closures.md +148 -0
  48. package/skills/bug-scan/references/time-numbers.md +158 -0
  49. package/skills/convert/SKILL.md +1463 -0
  50. package/skills/convert/agents/openai.yaml +6 -0
  51. package/skills/convert/references/legacy-overlap-migration.md +179 -0
  52. package/skills/deep-code-scan/SKILL.md +378 -0
  53. package/skills/deep-code-scan/agents/openai.yaml +6 -0
  54. package/skills/deep-code-scan/references/artifact-ledger.md +148 -0
  55. package/skills/deep-code-scan/references/consolidation.md +91 -0
  56. package/skills/deep-code-scan/references/decomposition.md +127 -0
  57. package/skills/deep-code-scan/references/item-templates.md +263 -0
  58. package/skills/deep-code-scan/references/lane-catalog.md +129 -0
  59. package/skills/deep-code-scan/references/review-gauntlet.md +108 -0
  60. package/skills/deep-code-scan/references/scanner-brief.md +152 -0
  61. package/skills/e2e-test-design/SKILL.md +517 -0
  62. package/skills/e2e-test-design/references/anti-tautology.md +261 -0
  63. package/skills/e2e-test-design/references/service-mocks.md +234 -0
  64. package/skills/e2e-test-design/references/test-taxonomy.md +211 -0
  65. package/skills/epic-design/SKILL.md +476 -0
  66. package/skills/epicize/SKILL.md +210 -0
  67. package/skills/epicize/agents/openai.yaml +6 -0
  68. package/skills/feature-design/SKILL.md +472 -0
  69. package/skills/fix/SKILL.md +173 -0
  70. package/skills/gate-cruft/SKILL.md +258 -0
  71. package/skills/gate-docs/SKILL.md +278 -0
  72. package/skills/gate-patterns/SKILL.md +389 -0
  73. package/skills/gate-refactor/SKILL.md +408 -0
  74. package/skills/gate-security/SKILL.md +275 -0
  75. package/skills/gate-tests/SKILL.md +367 -0
  76. package/skills/groom/SKILL.md +162 -0
  77. package/skills/groom/agents/openai.yaml +6 -0
  78. package/skills/ideate/SKILL.md +185 -0
  79. package/skills/ideate/agents/openai.yaml +6 -0
  80. package/skills/implement/SKILL.md +288 -0
  81. package/skills/implement-orchestrator/SKILL.md +604 -0
  82. package/skills/park/SKILL.md +111 -0
  83. package/skills/perf-design/SKILL.md +427 -0
  84. package/skills/perf-scout/SKILL.md +404 -0
  85. package/skills/perf-scout/agents/openai.yaml +6 -0
  86. package/skills/perf-scout/references/algorithmic-and-data-structures.md +126 -0
  87. package/skills/perf-scout/references/approximation-and-precomputation.md +136 -0
  88. package/skills/perf-scout/references/caching-and-memoization.md +123 -0
  89. package/skills/perf-scout/references/compiler-runtime-and-language.md +136 -0
  90. package/skills/perf-scout/references/database-and-storage-internals.md +144 -0
  91. package/skills/perf-scout/references/distributed-systems.md +150 -0
  92. package/skills/perf-scout/references/game-engine-and-realtime.md +136 -0
  93. package/skills/perf-scout/references/gpu-and-accelerators.md +130 -0
  94. package/skills/perf-scout/references/idea-ranking.md +75 -0
  95. package/skills/perf-scout/references/io-and-batching.md +135 -0
  96. package/skills/perf-scout/references/memory-and-data-locality.md +137 -0
  97. package/skills/perf-scout/references/parallelism-and-vectorization.md +126 -0
  98. package/skills/perf-scout/references/parked-item-template.md +117 -0
  99. package/skills/perf-scout/references/peer-review-pass.md +150 -0
  100. package/skills/perf-scout/references/report-template.md +148 -0
  101. package/skills/principles/SKILL.md +678 -0
  102. package/skills/principles/references/models.md +191 -0
  103. package/skills/principles/references/subagents.md +158 -0
  104. package/skills/prose-author/SKILL.md +185 -0
  105. package/skills/refactor-conventions-creator/SKILL.md +359 -0
  106. package/skills/refactor-conventions-creator/agents/openai.yaml +6 -0
  107. package/skills/refactor-conventions-creator/references/common-structures.md +91 -0
  108. package/skills/refactor-conventions-creator/references/common-styles.md +90 -0
  109. package/skills/refactor-design/SKILL.md +384 -0
  110. package/skills/release-deploy/SKILL.md +667 -0
  111. package/skills/release-deploy/agents/openai.yaml +6 -0
  112. package/skills/research/SKILL.md +166 -0
  113. package/skills/research/agents/openai.yaml +6 -0
  114. package/skills/review/SKILL.md +267 -0
  115. package/skills/review/references/deep-review.md +98 -0
  116. package/skills/review/references/review-lenses.md +68 -0
  117. package/skills/review/references/substrate-side-effects.md +161 -0
  118. package/skills/review/references/target-resolution.md +52 -0
  119. package/skills/scope/SKILL.md +486 -0
  120. package/work-view/Cargo.lock +486 -0
  121. package/work-view/Cargo.toml +9 -0
  122. package/work-view/crates/cli/.work-view-version +1 -0
  123. package/work-view/crates/cli/Cargo.toml +18 -0
  124. package/work-view/crates/cli/src/actionable.rs +500 -0
  125. package/work-view/crates/cli/src/args.rs +792 -0
  126. package/work-view/crates/cli/src/board/assets/board.css +855 -0
  127. package/work-view/crates/cli/src/board/assets/board.js +259 -0
  128. package/work-view/crates/cli/src/board/assets/card.js +139 -0
  129. package/work-view/crates/cli/src/board/assets/components.css +290 -0
  130. package/work-view/crates/cli/src/board/assets/dependency.js +1193 -0
  131. package/work-view/crates/cli/src/board/assets/detail.js +256 -0
  132. package/work-view/crates/cli/src/board/assets/filters.js +389 -0
  133. package/work-view/crates/cli/src/board/assets/index.html +77 -0
  134. package/work-view/crates/cli/src/board/assets/kanban.js +231 -0
  135. package/work-view/crates/cli/src/board/assets/markdown.js +260 -0
  136. package/work-view/crates/cli/src/board/assets/motion.css +131 -0
  137. package/work-view/crates/cli/src/board/assets/state.js +245 -0
  138. package/work-view/crates/cli/src/board/assets/table.js +304 -0
  139. package/work-view/crates/cli/src/board/assets/tokens.css +602 -0
  140. package/work-view/crates/cli/src/board/assets/views.js +41 -0
  141. package/work-view/crates/cli/src/board/assets.rs +162 -0
  142. package/work-view/crates/cli/src/board/feed.rs +204 -0
  143. package/work-view/crates/cli/src/board/mod.rs +246 -0
  144. package/work-view/crates/cli/src/board/open.rs +144 -0
  145. package/work-view/crates/cli/src/board/server.rs +407 -0
  146. package/work-view/crates/cli/src/main.rs +195 -0
  147. package/work-view/crates/cli/src/render.rs +354 -0
  148. package/work-view/crates/cli/src/scope.rs +157 -0
  149. package/work-view/crates/cli/src/stale.rs +581 -0
  150. package/work-view/crates/cli/tests/board-js/dependency-table.test.mjs +64 -0
  151. package/work-view/crates/cli/tests/board-js/expanded-browsing.test.mjs +79 -0
  152. package/work-view/crates/cli/tests/board-js/harness-smoke.test.mjs +11 -0
  153. package/work-view/crates/cli/tests/board-js/harness.mjs +437 -0
  154. package/work-view/crates/cli/tests/board-js/kanban-detail.test.mjs +75 -0
  155. package/work-view/crates/cli/tests/board-js/markdown-filter.test.mjs +60 -0
  156. package/work-view/crates/cli/tests/fixtures/golden/.work/CONVENTIONS.md +3 -0
  157. package/work-view/crates/cli/tests/fixtures/golden/.work/active/epics/epic-alpha.md +16 -0
  158. package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-a.md +16 -0
  159. package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-b.md +16 -0
  160. package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-alpha-1.md +16 -0
  161. package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-research-1.md +19 -0
  162. package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-done.md +16 -0
  163. package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-shipped.md +18 -0
  164. package/work-view/crates/cli/tests/fixtures/golden/.work/backlog/idea-backlog.md +7 -0
  165. package/work-view/crates/cli/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
  166. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/CONVENTIONS.md +6 -0
  167. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-dep-done.md +17 -0
  168. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-design-ready.md +17 -0
  169. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-impl-ready.md +17 -0
  170. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/stories/story-review-ready.md +17 -0
  171. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/CONVENTIONS.md +4 -0
  172. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/epics/malformed-no-closing-fence.md +8 -0
  173. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/features/good-item.md +16 -0
  174. package/work-view/crates/cli/tests/integration.rs +2833 -0
  175. package/work-view/crates/core/Cargo.toml +13 -0
  176. package/work-view/crates/core/src/error.rs +61 -0
  177. package/work-view/crates/core/src/filter.rs +804 -0
  178. package/work-view/crates/core/src/graph.rs +324 -0
  179. package/work-view/crates/core/src/index.rs +590 -0
  180. package/work-view/crates/core/src/lib.rs +54 -0
  181. package/work-view/crates/core/src/model.rs +202 -0
  182. package/work-view/crates/core/src/parse.rs +494 -0
  183. package/work-view/crates/core/tests/fixtures/golden/.work/CONVENTIONS.md +15 -0
  184. package/work-view/crates/core/tests/fixtures/golden/.work/active/epics/epic-core.md +16 -0
  185. package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-parser.md +16 -0
  186. package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-query.md +16 -0
  187. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/malformed-no-id.md +15 -0
  188. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-parse-unit.md +16 -0
  189. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-security-gate.md +16 -0
  190. package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-old.md +16 -0
  191. package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-parser.md +17 -0
  192. package/work-view/crates/core/tests/fixtures/golden/.work/backlog/idea-future-board.md +7 -0
  193. package/work-view/crates/core/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
  194. package/work-view/crates/core/tests/fixtures/precedence/.work/CONVENTIONS.md +10 -0
  195. package/work-view/crates/core/tests/fixtures/precedence/.work/archive/dup-item.md +18 -0
  196. package/work-view/crates/core/tests/fixtures/precedence/.work/releases/v2.0/dup-item.md +18 -0
  197. package/work-view/crates/core/tests/integration.rs +541 -0
  198. package/work-view/dist/.gitattributes +1 -0
  199. package/work-view/dist/README.md +42 -0
  200. package/work-view/dist/aarch64-apple-darwin/work-view +0 -0
  201. package/work-view/dist/aarch64-unknown-linux-musl/work-view +0 -0
  202. package/work-view/dist/x86_64-apple-darwin/work-view +0 -0
  203. package/work-view/dist/x86_64-unknown-linux-musl/work-view +0 -0
@@ -0,0 +1,500 @@
1
+ //! Actionable / dependency-view post-filter.
2
+ //!
3
+ //! Implements autopilot's readiness definition: an item is actionable when
4
+ //! - it lives in the **active tier** (`Tier::Active`), AND
5
+ //! - its **stage** is one of `drafting`, `implementing`, or `review`
6
+ //! (the three stages that admit a next move), AND
7
+ //! - for `--ready`: all `depends_on` are terminal (`deps_satisfied`)
8
+ //! - for `--blocked`: at least one `depends_on` is non-terminal (`!deps_satisfied`)
9
+ //!
10
+ //! The active-tier gate is essential: the core loads all four tiers, so without
11
+ //! it a stale archive/releases item with an odd stage could leak into `--ready`.
12
+ //!
13
+ //! Empty `depends_on` is vacuously satisfied → ready (correct: a drafting item
14
+ //! with no deps is design-ready).
15
+ //!
16
+ //! This is a post-filter over `query()` results, so it composes with every
17
+ //! standard filter by AND and never rewrites `filter.stage`. That means:
18
+ //! - `--ready --stage implementing` reproduces the old narrow (implementing-only) set.
19
+ //! - `--ready --stage drafting` = design-ready only.
20
+
21
+ use work_view_core::index::Substrate;
22
+ use work_view_core::model::{Item, Tier};
23
+
24
+ use crate::args::DependencyView;
25
+
26
+ // ── Predicate ─────────────────────────────────────────────────────────────────
27
+
28
+ /// Returns `true` if `item` is a candidate for the ready/blocked computation.
29
+ ///
30
+ /// Candidates are active-tier items at a movable stage. This mirrors the
31
+ /// autopilot queue definition exactly.
32
+ fn is_actionable_candidate(item: &Item) -> bool {
33
+ item.tier == Tier::Active
34
+ // `[scan]`-tagged items are engagement-owned by the deep-code-scan
35
+ // skill: they are never surfaced by `--ready`/`--blocked` nor grabbed
36
+ // by autopilot, so they are categorically excluded from actionability.
37
+ && !item.tags.iter().any(|t| t == "scan")
38
+ && matches!(
39
+ item.stage.as_deref(),
40
+ Some("drafting" | "implementing" | "review")
41
+ )
42
+ }
43
+
44
+ /// Return the CLI-equivalent ready/blocked booleans for a single item.
45
+ pub(crate) fn dependency_status(sub: &Substrate, item: &Item) -> (bool, bool) {
46
+ let actionable = is_actionable_candidate(item);
47
+ let ready = actionable && sub.deps_satisfied(item);
48
+ let blocked = actionable && !sub.deps_satisfied(item);
49
+ (ready, blocked)
50
+ }
51
+
52
+ // ── Public post-filter ────────────────────────────────────────────────────────
53
+
54
+ /// Post-filter over `query()` results; preserves input (load) order.
55
+ ///
56
+ /// - `All` → no change; return items as-is.
57
+ /// - `Ready` → keep only candidates whose deps are all terminal.
58
+ /// - `Blocked` → keep only candidates with at least one non-terminal dep.
59
+ ///
60
+ /// Correctness note: `sub.deps_satisfied` evaluates over the **whole substrate**,
61
+ /// not the filtered subset. This is intentional — a dep that is filtered out
62
+ /// by another flag (e.g. `--stage`) is still terminal if it is done/released.
63
+ pub fn apply_dependency_view<'a>(
64
+ sub: &Substrate,
65
+ items: Vec<&'a Item>,
66
+ view: DependencyView,
67
+ ) -> Vec<&'a Item> {
68
+ match view {
69
+ DependencyView::All => items,
70
+ DependencyView::Ready => items
71
+ .into_iter()
72
+ .filter(|i| dependency_status(sub, i).0)
73
+ .collect(),
74
+ DependencyView::Blocked => items
75
+ .into_iter()
76
+ .filter(|i| dependency_status(sub, i).1)
77
+ .collect(),
78
+ }
79
+ }
80
+
81
+ // ── Tests ─────────────────────────────────────────────────────────────────────
82
+
83
+ #[cfg(test)]
84
+ mod tests {
85
+ use super::*;
86
+ use std::fs;
87
+ use tempfile::TempDir;
88
+ use work_view_core::{
89
+ filter::Filter,
90
+ index::Substrate,
91
+ model::{Item, Tier},
92
+ };
93
+
94
+ // ── Fixture helpers ───────────────────────────────────────────────────────
95
+
96
+ fn setup_substrate(items: &[(&str, &str)]) -> (TempDir, Substrate) {
97
+ let tmp = TempDir::new().unwrap();
98
+ let root = tmp.path().to_path_buf();
99
+ fs::create_dir_all(root.join(".work")).unwrap();
100
+ fs::write(root.join(".work/CONVENTIONS.md"), "# Conventions\n").unwrap();
101
+ for (rel_path, content) in items {
102
+ let path = root.join(".work").join(rel_path);
103
+ if let Some(p) = path.parent() {
104
+ fs::create_dir_all(p).unwrap();
105
+ }
106
+ fs::write(&path, content).unwrap();
107
+ }
108
+ let (sub, _) = Substrate::load(&root).unwrap();
109
+ (tmp, sub)
110
+ }
111
+
112
+ fn item_md(
113
+ id: &str,
114
+ tier_dir: &str,
115
+ stage: &str,
116
+ depends_on: &[&str],
117
+ ) -> (&'static str, String) {
118
+ // caller provides tier_dir like "active/features", "archive", etc.
119
+ let deps = if depends_on.is_empty() {
120
+ "[]".to_string()
121
+ } else {
122
+ format!(
123
+ "[{}]",
124
+ depends_on
125
+ .iter()
126
+ .map(|d| format!("\"{d}\""))
127
+ .collect::<Vec<_>>()
128
+ .join(", ")
129
+ )
130
+ };
131
+ let content = format!(
132
+ "---\nid: {id}\nkind: feature\nstage: {stage}\ntags: []\nparent: null\ndepends_on: {deps}\nrelease_binding: null\ngate_origin: null\ncreated: 2026-01-01\nupdated: 2026-01-01\n---\n\n# {id}\n"
133
+ );
134
+ // leak is fine in tests (small, bounded)
135
+ let path = Box::leak(format!("{tier_dir}/{id}.md").into_boxed_str());
136
+ (path, content)
137
+ }
138
+
139
+ /// Run `apply_dependency_view` on all items from a substrate.
140
+ fn run(sub: &Substrate, view: DependencyView) -> Vec<String> {
141
+ let items = sub.query(&Filter::default());
142
+ apply_dependency_view(sub, items, view)
143
+ .into_iter()
144
+ .map(|i| i.id.clone())
145
+ .collect()
146
+ }
147
+
148
+ // ── is_actionable_candidate ───────────────────────────────────────────────
149
+
150
+ // We test the predicate indirectly through apply_dependency_view.
151
+ // Direct unit tests below use a helper that creates Items in-memory.
152
+
153
+ fn make_item_direct(id: &str, tier: Tier, stage: Option<&str>) -> Item {
154
+ use std::path::PathBuf;
155
+ Item {
156
+ id: id.to_string(),
157
+ kind: Some("feature".into()),
158
+ stage: stage.map(str::to_string),
159
+ tags: vec![],
160
+ parent: None,
161
+ depends_on: vec![],
162
+ release_binding: None,
163
+ gate_origin: None,
164
+ research_refs: vec![],
165
+ research_origin: None,
166
+ scan_origin: None,
167
+ created: None,
168
+ updated: None,
169
+ tier,
170
+ path: PathBuf::from("/fake"),
171
+ rel_path: PathBuf::from(".work/active/features/fake.md"),
172
+ raw_text: String::new(),
173
+ body: String::new(),
174
+ }
175
+ }
176
+
177
+ #[test]
178
+ fn active_drafting_is_candidate() {
179
+ assert!(is_actionable_candidate(&make_item_direct(
180
+ "x",
181
+ Tier::Active,
182
+ Some("drafting")
183
+ )));
184
+ }
185
+
186
+ #[test]
187
+ fn active_implementing_is_candidate() {
188
+ assert!(is_actionable_candidate(&make_item_direct(
189
+ "x",
190
+ Tier::Active,
191
+ Some("implementing")
192
+ )));
193
+ }
194
+
195
+ #[test]
196
+ fn active_review_is_candidate() {
197
+ assert!(is_actionable_candidate(&make_item_direct(
198
+ "x",
199
+ Tier::Active,
200
+ Some("review")
201
+ )));
202
+ }
203
+
204
+ #[test]
205
+ fn active_done_is_not_candidate() {
206
+ assert!(!is_actionable_candidate(&make_item_direct(
207
+ "x",
208
+ Tier::Active,
209
+ Some("done")
210
+ )));
211
+ }
212
+
213
+ #[test]
214
+ fn active_no_stage_is_not_candidate() {
215
+ assert!(!is_actionable_candidate(&make_item_direct(
216
+ "x",
217
+ Tier::Active,
218
+ None
219
+ )));
220
+ }
221
+
222
+ #[test]
223
+ fn backlog_implementing_is_not_candidate() {
224
+ // backlog tier — not active
225
+ assert!(!is_actionable_candidate(&make_item_direct(
226
+ "x",
227
+ Tier::Backlog,
228
+ Some("implementing")
229
+ )));
230
+ }
231
+
232
+ #[test]
233
+ fn archive_implementing_is_not_candidate() {
234
+ // archive tier — not active
235
+ assert!(!is_actionable_candidate(&make_item_direct(
236
+ "x",
237
+ Tier::Archive,
238
+ Some("implementing")
239
+ )));
240
+ }
241
+
242
+ #[test]
243
+ fn releases_implementing_is_not_candidate() {
244
+ // releases tier — not active
245
+ assert!(!is_actionable_candidate(&make_item_direct(
246
+ "x",
247
+ Tier::Releases,
248
+ Some("implementing")
249
+ )));
250
+ }
251
+
252
+ #[test]
253
+ fn scan_tagged_active_implementing_is_not_candidate() {
254
+ // A `[scan]`-tagged item is engagement-owned by the deep-code-scan
255
+ // skill: it must never be surfaced by --ready/--blocked, even though it
256
+ // is Active and at a movable stage. An otherwise-identical untagged
257
+ // item IS a candidate.
258
+ let mut tagged = make_item_direct("scan-x", Tier::Active, Some("implementing"));
259
+ tagged.tags = vec!["scan".to_string()];
260
+ assert!(
261
+ !is_actionable_candidate(&tagged),
262
+ "scan-tagged active implementing item must NOT be actionable"
263
+ );
264
+
265
+ let untagged = make_item_direct("plain-x", Tier::Active, Some("implementing"));
266
+ assert!(
267
+ is_actionable_candidate(&untagged),
268
+ "untagged active implementing item must be actionable"
269
+ );
270
+ }
271
+
272
+ // ── apply_dependency_view: All ────────────────────────────────────────────
273
+
274
+ #[test]
275
+ fn all_returns_everything() {
276
+ let (path_a, content_a) = item_md("a", "active/features", "implementing", &[]);
277
+ let (_tmp, sub) = setup_substrate(&[(path_a, &content_a)]);
278
+ let ids = run(&sub, DependencyView::All);
279
+ assert!(ids.contains(&"a".to_string()));
280
+ }
281
+
282
+ // ── apply_dependency_view: Ready ──────────────────────────────────────────
283
+
284
+ #[test]
285
+ fn ready_includes_drafting_with_satisfied_deps() {
286
+ let (path_done, content_done) = item_md("dep-done", "active/features", "done", &[]);
287
+ let (path_a, content_a) = item_md("a", "active/features", "drafting", &["dep-done"]);
288
+ let (_tmp, sub) = setup_substrate(&[(path_done, &content_done), (path_a, &content_a)]);
289
+ let ids = run(&sub, DependencyView::Ready);
290
+ assert!(
291
+ ids.contains(&"a".to_string()),
292
+ "drafting item with done dep should be ready"
293
+ );
294
+ }
295
+
296
+ #[test]
297
+ fn ready_includes_implementing_with_satisfied_deps() {
298
+ let (path_done, content_done) = item_md("dep-done", "active/features", "done", &[]);
299
+ let (path_a, content_a) = item_md("a", "active/features", "implementing", &["dep-done"]);
300
+ let (_tmp, sub) = setup_substrate(&[(path_done, &content_done), (path_a, &content_a)]);
301
+ let ids = run(&sub, DependencyView::Ready);
302
+ assert!(
303
+ ids.contains(&"a".to_string()),
304
+ "implementing item with done dep should be ready"
305
+ );
306
+ }
307
+
308
+ #[test]
309
+ fn ready_includes_review_with_satisfied_deps() {
310
+ let (path_a, content_a) = item_md("a", "active/features", "review", &[]);
311
+ let (_tmp, sub) = setup_substrate(&[(path_a, &content_a)]);
312
+ let ids = run(&sub, DependencyView::Ready);
313
+ assert!(
314
+ ids.contains(&"a".to_string()),
315
+ "review item with empty deps (vacuously satisfied) should be ready"
316
+ );
317
+ }
318
+
319
+ #[test]
320
+ fn ready_empty_depends_on_is_vacuously_satisfied() {
321
+ let (path_a, content_a) = item_md("a", "active/features", "implementing", &[]);
322
+ let (_tmp, sub) = setup_substrate(&[(path_a, &content_a)]);
323
+ let ids = run(&sub, DependencyView::Ready);
324
+ assert!(
325
+ ids.contains(&"a".to_string()),
326
+ "item with empty depends_on should be ready"
327
+ );
328
+ }
329
+
330
+ #[test]
331
+ fn ready_excludes_implementing_with_unmet_dep() {
332
+ let (path_blocked, content_blocked) =
333
+ item_md("blocked-dep", "active/features", "implementing", &[]);
334
+ let (path_a, content_a) = item_md("a", "active/features", "implementing", &["blocked-dep"]);
335
+ let (_tmp, sub) =
336
+ setup_substrate(&[(path_blocked, &content_blocked), (path_a, &content_a)]);
337
+ let ids = run(&sub, DependencyView::Ready);
338
+ assert!(
339
+ !ids.contains(&"a".to_string()),
340
+ "item with unmet dep should NOT be ready"
341
+ );
342
+ }
343
+
344
+ #[test]
345
+ fn ready_excludes_review_with_unmet_dep() {
346
+ let (path_blocked, content_blocked) =
347
+ item_md("blocked-dep", "active/features", "implementing", &[]);
348
+ let (path_a, content_a) = item_md("a", "active/features", "review", &["blocked-dep"]);
349
+ let (_tmp, sub) =
350
+ setup_substrate(&[(path_blocked, &content_blocked), (path_a, &content_a)]);
351
+ let ids = run(&sub, DependencyView::Ready);
352
+ assert!(
353
+ !ids.contains(&"a".to_string()),
354
+ "review item with unmet dep should NOT be ready"
355
+ );
356
+ }
357
+
358
+ #[test]
359
+ fn ready_excludes_done_active_item() {
360
+ let (path_a, content_a) = item_md("a", "active/features", "done", &[]);
361
+ let (_tmp, sub) = setup_substrate(&[(path_a, &content_a)]);
362
+ let ids = run(&sub, DependencyView::Ready);
363
+ assert!(
364
+ !ids.contains(&"a".to_string()),
365
+ "done item should NOT be ready"
366
+ );
367
+ }
368
+
369
+ #[test]
370
+ fn ready_excludes_archive_items_even_with_movable_stage() {
371
+ let (path_a, content_a) = item_md("a", "archive", "implementing", &[]);
372
+ let (_tmp, sub) = setup_substrate(&[(path_a, &content_a)]);
373
+ let ids = run(&sub, DependencyView::Ready);
374
+ assert!(
375
+ !ids.contains(&"a".to_string()),
376
+ "archive item should NOT be ready (tier gate)"
377
+ );
378
+ }
379
+
380
+ #[test]
381
+ fn ready_excludes_backlog_items() {
382
+ // Backlog items don't have stage at all (set to "implementing" here
383
+ // to test the tier gate is what excludes, not the stage check)
384
+ let (path_a, content_a) = item_md("a", "backlog", "implementing", &[]);
385
+ let (_tmp, sub) = setup_substrate(&[(path_a, &content_a)]);
386
+ let ids = run(&sub, DependencyView::Ready);
387
+ assert!(
388
+ !ids.contains(&"a".to_string()),
389
+ "backlog item should NOT be ready (tier gate)"
390
+ );
391
+ }
392
+
393
+ #[test]
394
+ fn ready_stage_filter_composes_correctly() {
395
+ // --ready --stage implementing should include only implementing items
396
+ // (not drafting or review), even though all three pass is_actionable_candidate.
397
+ let (path_a, content_a) = item_md("a-drafting", "active/features", "drafting", &[]);
398
+ let (path_b, content_b) = item_md("b-implementing", "active/features", "implementing", &[]);
399
+ let (path_c, content_c) = item_md("c-review", "active/features", "review", &[]);
400
+ let (_tmp, sub) = setup_substrate(&[
401
+ (path_a, &content_a),
402
+ (path_b, &content_b),
403
+ (path_c, &content_c),
404
+ ]);
405
+ // Simulate --ready --stage implementing: pre-filter by stage, then apply_dependency_view
406
+ use work_view_core::filter::Match;
407
+ let f = Filter {
408
+ stage: Match::Equals("implementing".into()),
409
+ ..Filter::default()
410
+ };
411
+ let items = sub.query(&f);
412
+ let filtered = apply_dependency_view(&sub, items, DependencyView::Ready);
413
+ let ids: Vec<&str> = filtered.iter().map(|i| i.id.as_str()).collect();
414
+ assert_eq!(
415
+ ids,
416
+ vec!["b-implementing"],
417
+ "should reproduce old narrow set"
418
+ );
419
+ }
420
+
421
+ // ── apply_dependency_view: Blocked ────────────────────────────────────────
422
+
423
+ #[test]
424
+ fn blocked_includes_implementing_with_unmet_dep() {
425
+ let (path_unmet, content_unmet) =
426
+ item_md("unmet-dep", "active/features", "implementing", &[]);
427
+ let (path_a, content_a) = item_md("a", "active/features", "implementing", &["unmet-dep"]);
428
+ let (_tmp, sub) = setup_substrate(&[(path_unmet, &content_unmet), (path_a, &content_a)]);
429
+ let ids = run(&sub, DependencyView::Blocked);
430
+ assert!(
431
+ ids.contains(&"a".to_string()),
432
+ "item with unmet dep should be blocked"
433
+ );
434
+ }
435
+
436
+ #[test]
437
+ fn blocked_includes_review_with_unmet_dep() {
438
+ // Decision-table cell: stage==review ∧ ¬deps_satisfied → --blocked.
439
+ // A review item whose dependency regressed (here: still implementing,
440
+ // non-terminal) must report blocked, mirroring the conservative
441
+ // deps_satisfied-uniform-across-stages invariant.
442
+ let (path_unmet, content_unmet) =
443
+ item_md("unmet-dep", "active/features", "implementing", &[]);
444
+ let (path_a, content_a) = item_md("a", "active/features", "review", &["unmet-dep"]);
445
+ let (_tmp, sub) = setup_substrate(&[(path_unmet, &content_unmet), (path_a, &content_a)]);
446
+ let ids = run(&sub, DependencyView::Blocked);
447
+ assert!(
448
+ ids.contains(&"a".to_string()),
449
+ "review item with unmet dep should be blocked"
450
+ );
451
+ }
452
+
453
+ #[test]
454
+ fn blocked_excludes_implementing_with_satisfied_deps() {
455
+ let (path_done, content_done) = item_md("dep-done", "active/features", "done", &[]);
456
+ let (path_a, content_a) = item_md("a", "active/features", "implementing", &["dep-done"]);
457
+ let (_tmp, sub) = setup_substrate(&[(path_done, &content_done), (path_a, &content_a)]);
458
+ let ids = run(&sub, DependencyView::Blocked);
459
+ assert!(
460
+ !ids.contains(&"a".to_string()),
461
+ "item with satisfied dep should NOT be blocked"
462
+ );
463
+ }
464
+
465
+ #[test]
466
+ fn blocked_excludes_done_items() {
467
+ let (path_a, content_a) = item_md("a", "active/features", "done", &[]);
468
+ let (_tmp, sub) = setup_substrate(&[(path_a, &content_a)]);
469
+ let ids = run(&sub, DependencyView::Blocked);
470
+ assert!(
471
+ !ids.contains(&"a".to_string()),
472
+ "done item should NOT be blocked"
473
+ );
474
+ }
475
+
476
+ #[test]
477
+ fn blocked_excludes_archive_items() {
478
+ let (path_unmet, content_unmet) =
479
+ item_md("unmet-dep", "active/features", "implementing", &[]);
480
+ let (path_a, content_a) = item_md("a", "archive", "implementing", &["unmet-dep"]);
481
+ let (_tmp, sub) = setup_substrate(&[(path_unmet, &content_unmet), (path_a, &content_a)]);
482
+ let ids = run(&sub, DependencyView::Blocked);
483
+ assert!(
484
+ !ids.contains(&"a".to_string()),
485
+ "archive item should NOT be blocked (tier gate)"
486
+ );
487
+ }
488
+
489
+ // ── Load order preserved ──────────────────────────────────────────────────
490
+
491
+ #[test]
492
+ fn load_order_preserved_in_ready() {
493
+ let (path_a, content_a) = item_md("a-first", "active/features", "implementing", &[]);
494
+ let (path_b, content_b) = item_md("b-second", "active/features", "implementing", &[]);
495
+ let (_tmp, sub) = setup_substrate(&[(path_a, &content_a), (path_b, &content_b)]);
496
+ let ids = run(&sub, DependencyView::Ready);
497
+ assert_eq!(ids[0], "a-first", "load order should be preserved");
498
+ assert_eq!(ids[1], "b-second", "load order should be preserved");
499
+ }
500
+ }