@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.
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +28 -0
- package/CHANGELOG.md +591 -0
- package/README.md +107 -0
- package/docs/ARCHITECTURE.md +716 -0
- package/docs/MIGRATION.md +419 -0
- package/docs/PRINCIPLES.md +464 -0
- package/docs/ROADMAP.md +95 -0
- package/docs/SPEC.md +855 -0
- package/docs/VISION.md +106 -0
- package/extensions/agile-workflow.test.ts +419 -0
- package/extensions/agile-workflow.ts +467 -0
- package/hooks/hooks.json +52 -0
- package/hooks/scripts/prompt-context.py +628 -0
- package/hooks/scripts/substrate-maintainer.py +359 -0
- package/hooks/scripts/test_prompt_context.py +757 -0
- package/package.json +30 -0
- package/scripts/install-work-view.sh +165 -0
- package/scripts/tests/agent-metadata.test.sh +75 -0
- package/scripts/tests/bump-version.test.sh +552 -0
- package/scripts/tests/channel-parity.test.sh +117 -0
- package/scripts/tests/convert-content-integrity.test.sh +246 -0
- package/scripts/tests/convert-install-routing.test.sh +159 -0
- package/scripts/tests/install-work-view.test.sh +411 -0
- package/scripts/tests/pi-package-metadata.test.sh +119 -0
- package/scripts/tests/work-board-shim.test.sh +239 -0
- package/scripts/tests/work-view-dist-version.test.sh +105 -0
- package/scripts/work-board.sh +64 -0
- package/scripts/work-view.sh +423 -0
- package/skills/autopilot/SKILL.md +330 -0
- package/skills/autopilot/agents/openai.yaml +6 -0
- package/skills/board/SKILL.md +62 -0
- package/skills/board/agents/openai.yaml +6 -0
- package/skills/bold-refactor/SKILL.md +244 -0
- package/skills/bold-refactor/agents/openai.yaml +6 -0
- package/skills/bug-scan/SKILL.md +378 -0
- package/skills/bug-scan/agents/openai.yaml +6 -0
- package/skills/bug-scan/references/async-promises.md +119 -0
- package/skills/bug-scan/references/concurrency-races.md +148 -0
- package/skills/bug-scan/references/data-layer.md +167 -0
- package/skills/bug-scan/references/error-handling.md +197 -0
- package/skills/bug-scan/references/gate-item-template.md +94 -0
- package/skills/bug-scan/references/language-footguns.md +219 -0
- package/skills/bug-scan/references/parked-item-template.md +103 -0
- package/skills/bug-scan/references/report-template.md +115 -0
- package/skills/bug-scan/references/resource-leaks.md +199 -0
- package/skills/bug-scan/references/state-closures.md +148 -0
- package/skills/bug-scan/references/time-numbers.md +158 -0
- package/skills/convert/SKILL.md +1463 -0
- package/skills/convert/agents/openai.yaml +6 -0
- package/skills/convert/references/legacy-overlap-migration.md +179 -0
- package/skills/deep-code-scan/SKILL.md +378 -0
- package/skills/deep-code-scan/agents/openai.yaml +6 -0
- package/skills/deep-code-scan/references/artifact-ledger.md +148 -0
- package/skills/deep-code-scan/references/consolidation.md +91 -0
- package/skills/deep-code-scan/references/decomposition.md +127 -0
- package/skills/deep-code-scan/references/item-templates.md +263 -0
- package/skills/deep-code-scan/references/lane-catalog.md +129 -0
- package/skills/deep-code-scan/references/review-gauntlet.md +108 -0
- package/skills/deep-code-scan/references/scanner-brief.md +152 -0
- package/skills/e2e-test-design/SKILL.md +517 -0
- package/skills/e2e-test-design/references/anti-tautology.md +261 -0
- package/skills/e2e-test-design/references/service-mocks.md +234 -0
- package/skills/e2e-test-design/references/test-taxonomy.md +211 -0
- package/skills/epic-design/SKILL.md +476 -0
- package/skills/epicize/SKILL.md +210 -0
- package/skills/epicize/agents/openai.yaml +6 -0
- package/skills/feature-design/SKILL.md +472 -0
- package/skills/fix/SKILL.md +173 -0
- package/skills/gate-cruft/SKILL.md +258 -0
- package/skills/gate-docs/SKILL.md +278 -0
- package/skills/gate-patterns/SKILL.md +389 -0
- package/skills/gate-refactor/SKILL.md +408 -0
- package/skills/gate-security/SKILL.md +275 -0
- package/skills/gate-tests/SKILL.md +367 -0
- package/skills/groom/SKILL.md +162 -0
- package/skills/groom/agents/openai.yaml +6 -0
- package/skills/ideate/SKILL.md +185 -0
- package/skills/ideate/agents/openai.yaml +6 -0
- package/skills/implement/SKILL.md +288 -0
- package/skills/implement-orchestrator/SKILL.md +604 -0
- package/skills/park/SKILL.md +111 -0
- package/skills/perf-design/SKILL.md +427 -0
- package/skills/perf-scout/SKILL.md +404 -0
- package/skills/perf-scout/agents/openai.yaml +6 -0
- package/skills/perf-scout/references/algorithmic-and-data-structures.md +126 -0
- package/skills/perf-scout/references/approximation-and-precomputation.md +136 -0
- package/skills/perf-scout/references/caching-and-memoization.md +123 -0
- package/skills/perf-scout/references/compiler-runtime-and-language.md +136 -0
- package/skills/perf-scout/references/database-and-storage-internals.md +144 -0
- package/skills/perf-scout/references/distributed-systems.md +150 -0
- package/skills/perf-scout/references/game-engine-and-realtime.md +136 -0
- package/skills/perf-scout/references/gpu-and-accelerators.md +130 -0
- package/skills/perf-scout/references/idea-ranking.md +75 -0
- package/skills/perf-scout/references/io-and-batching.md +135 -0
- package/skills/perf-scout/references/memory-and-data-locality.md +137 -0
- package/skills/perf-scout/references/parallelism-and-vectorization.md +126 -0
- package/skills/perf-scout/references/parked-item-template.md +117 -0
- package/skills/perf-scout/references/peer-review-pass.md +150 -0
- package/skills/perf-scout/references/report-template.md +148 -0
- package/skills/principles/SKILL.md +678 -0
- package/skills/principles/references/models.md +191 -0
- package/skills/principles/references/subagents.md +158 -0
- package/skills/prose-author/SKILL.md +185 -0
- package/skills/refactor-conventions-creator/SKILL.md +359 -0
- package/skills/refactor-conventions-creator/agents/openai.yaml +6 -0
- package/skills/refactor-conventions-creator/references/common-structures.md +91 -0
- package/skills/refactor-conventions-creator/references/common-styles.md +90 -0
- package/skills/refactor-design/SKILL.md +384 -0
- package/skills/release-deploy/SKILL.md +667 -0
- package/skills/release-deploy/agents/openai.yaml +6 -0
- package/skills/research/SKILL.md +166 -0
- package/skills/research/agents/openai.yaml +6 -0
- package/skills/review/SKILL.md +267 -0
- package/skills/review/references/deep-review.md +98 -0
- package/skills/review/references/review-lenses.md +68 -0
- package/skills/review/references/substrate-side-effects.md +161 -0
- package/skills/review/references/target-resolution.md +52 -0
- package/skills/scope/SKILL.md +486 -0
- package/work-view/Cargo.lock +486 -0
- package/work-view/Cargo.toml +9 -0
- package/work-view/crates/cli/.work-view-version +1 -0
- package/work-view/crates/cli/Cargo.toml +18 -0
- package/work-view/crates/cli/src/actionable.rs +500 -0
- package/work-view/crates/cli/src/args.rs +792 -0
- package/work-view/crates/cli/src/board/assets/board.css +855 -0
- package/work-view/crates/cli/src/board/assets/board.js +259 -0
- package/work-view/crates/cli/src/board/assets/card.js +139 -0
- package/work-view/crates/cli/src/board/assets/components.css +290 -0
- package/work-view/crates/cli/src/board/assets/dependency.js +1193 -0
- package/work-view/crates/cli/src/board/assets/detail.js +256 -0
- package/work-view/crates/cli/src/board/assets/filters.js +389 -0
- package/work-view/crates/cli/src/board/assets/index.html +77 -0
- package/work-view/crates/cli/src/board/assets/kanban.js +231 -0
- package/work-view/crates/cli/src/board/assets/markdown.js +260 -0
- package/work-view/crates/cli/src/board/assets/motion.css +131 -0
- package/work-view/crates/cli/src/board/assets/state.js +245 -0
- package/work-view/crates/cli/src/board/assets/table.js +304 -0
- package/work-view/crates/cli/src/board/assets/tokens.css +602 -0
- package/work-view/crates/cli/src/board/assets/views.js +41 -0
- package/work-view/crates/cli/src/board/assets.rs +162 -0
- package/work-view/crates/cli/src/board/feed.rs +204 -0
- package/work-view/crates/cli/src/board/mod.rs +246 -0
- package/work-view/crates/cli/src/board/open.rs +144 -0
- package/work-view/crates/cli/src/board/server.rs +407 -0
- package/work-view/crates/cli/src/main.rs +195 -0
- package/work-view/crates/cli/src/render.rs +354 -0
- package/work-view/crates/cli/src/scope.rs +157 -0
- package/work-view/crates/cli/src/stale.rs +581 -0
- package/work-view/crates/cli/tests/board-js/dependency-table.test.mjs +64 -0
- package/work-view/crates/cli/tests/board-js/expanded-browsing.test.mjs +79 -0
- package/work-view/crates/cli/tests/board-js/harness-smoke.test.mjs +11 -0
- package/work-view/crates/cli/tests/board-js/harness.mjs +437 -0
- package/work-view/crates/cli/tests/board-js/kanban-detail.test.mjs +75 -0
- package/work-view/crates/cli/tests/board-js/markdown-filter.test.mjs +60 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/CONVENTIONS.md +3 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/epics/epic-alpha.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-a.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-b.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-alpha-1.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-research-1.md +19 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-done.md +16 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-shipped.md +18 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/backlog/idea-backlog.md +7 -0
- package/work-view/crates/cli/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/CONVENTIONS.md +6 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-dep-done.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-design-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-impl-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/stories/story-review-ready.md +17 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/CONVENTIONS.md +4 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/epics/malformed-no-closing-fence.md +8 -0
- package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/features/good-item.md +16 -0
- package/work-view/crates/cli/tests/integration.rs +2833 -0
- package/work-view/crates/core/Cargo.toml +13 -0
- package/work-view/crates/core/src/error.rs +61 -0
- package/work-view/crates/core/src/filter.rs +804 -0
- package/work-view/crates/core/src/graph.rs +324 -0
- package/work-view/crates/core/src/index.rs +590 -0
- package/work-view/crates/core/src/lib.rs +54 -0
- package/work-view/crates/core/src/model.rs +202 -0
- package/work-view/crates/core/src/parse.rs +494 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/CONVENTIONS.md +15 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/epics/epic-core.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-parser.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-query.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/malformed-no-id.md +15 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-parse-unit.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-security-gate.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-old.md +16 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-parser.md +17 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/backlog/idea-future-board.md +7 -0
- package/work-view/crates/core/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/CONVENTIONS.md +10 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/archive/dup-item.md +18 -0
- package/work-view/crates/core/tests/fixtures/precedence/.work/releases/v2.0/dup-item.md +18 -0
- package/work-view/crates/core/tests/integration.rs +541 -0
- package/work-view/dist/.gitattributes +1 -0
- package/work-view/dist/README.md +42 -0
- package/work-view/dist/aarch64-apple-darwin/work-view +0 -0
- package/work-view/dist/aarch64-unknown-linux-musl/work-view +0 -0
- package/work-view/dist/x86_64-apple-darwin/work-view +0 -0
- 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
|
+
}
|