@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,541 @@
|
|
|
1
|
+
//! Integration tests over fixture `.work/` trees.
|
|
2
|
+
//!
|
|
3
|
+
//! The "golden" fixture exercises the full pipeline: active items across
|
|
4
|
+
//! {epics,features,stories}, a backlog-minimal item, an archive item, a
|
|
5
|
+
//! releases item, a malformed file (no id), and a cross-tier duplicate id —
|
|
6
|
+
//! all from a single `Substrate::load` call.
|
|
7
|
+
|
|
8
|
+
use std::path::Path;
|
|
9
|
+
use work_view_core::{
|
|
10
|
+
filter::{Filter, Match},
|
|
11
|
+
index::{find_substrate_root, Substrate},
|
|
12
|
+
model::Tier,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/// Path to the golden fixture substrate root.
|
|
16
|
+
fn golden_root() -> &'static Path {
|
|
17
|
+
Path::new(concat!(
|
|
18
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
19
|
+
"/tests/fixtures/golden"
|
|
20
|
+
))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Path to the precedence fixture substrate root.
|
|
24
|
+
///
|
|
25
|
+
/// Holds the same id (`dup-item`) in BOTH `releases/` and `archive/` — the one
|
|
26
|
+
/// duplicate pairing where byte-sorted load order and tier precedence disagree.
|
|
27
|
+
fn precedence_root() -> &'static Path {
|
|
28
|
+
Path::new(concat!(
|
|
29
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
30
|
+
"/tests/fixtures/precedence"
|
|
31
|
+
))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Golden fixture: full pipeline ─────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn golden_load_succeeds() {
|
|
38
|
+
let (sub, report) = Substrate::load(golden_root()).unwrap();
|
|
39
|
+
|
|
40
|
+
// Active epics/features/stories + backlog + releases + archive (minus malformed)
|
|
41
|
+
// Expected items: epic-core, feat-parser(active), feat-query, story-parse-unit,
|
|
42
|
+
// story-security-gate, idea-future-board(backlog),
|
|
43
|
+
// release-v1.0(releases), feat-old(archive),
|
|
44
|
+
// feat-parser(archive) — duplicate, stays in items.
|
|
45
|
+
// Malformed file (malformed-no-id.md) is excluded.
|
|
46
|
+
let item_count = sub.items().len();
|
|
47
|
+
// 9 valid items (8 distinct + 1 duplicate archive copy of feat-parser)
|
|
48
|
+
assert_eq!(item_count, 9, "expected 9 valid items, got {item_count}");
|
|
49
|
+
|
|
50
|
+
// The malformed file should produce exactly one parse error
|
|
51
|
+
assert_eq!(
|
|
52
|
+
report.parse_errors.len(),
|
|
53
|
+
1,
|
|
54
|
+
"expected 1 parse error for malformed-no-id.md"
|
|
55
|
+
);
|
|
56
|
+
assert!(report.parse_errors[0]
|
|
57
|
+
.path
|
|
58
|
+
.to_str()
|
|
59
|
+
.unwrap()
|
|
60
|
+
.contains("malformed-no-id.md"));
|
|
61
|
+
|
|
62
|
+
// The duplicate feat-parser (archive copy) should produce one duplicate diagnostic
|
|
63
|
+
assert_eq!(
|
|
64
|
+
report.duplicate_ids.len(),
|
|
65
|
+
1,
|
|
66
|
+
"expected 1 duplicate-id diagnostic"
|
|
67
|
+
);
|
|
68
|
+
let dup = &report.duplicate_ids[0];
|
|
69
|
+
assert_eq!(dup.id.as_deref(), Some("feat-parser"));
|
|
70
|
+
// The duplicate diagnostic is for the non-canonical (lower-precedence) copy
|
|
71
|
+
assert_eq!(dup.tier, Tier::Archive);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[test]
|
|
75
|
+
fn golden_by_id_active_wins_over_archive() {
|
|
76
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
77
|
+
|
|
78
|
+
// feat-parser exists in both active/ and archive/; active wins
|
|
79
|
+
let item = sub.by_id("feat-parser").unwrap();
|
|
80
|
+
assert_eq!(
|
|
81
|
+
item.tier,
|
|
82
|
+
Tier::Active,
|
|
83
|
+
"active tier copy must win by_id resolution"
|
|
84
|
+
);
|
|
85
|
+
assert_eq!(item.stage.as_deref(), Some("implementing"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#[test]
|
|
89
|
+
fn golden_tiers_assigned_correctly() {
|
|
90
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
91
|
+
|
|
92
|
+
assert_eq!(sub.by_id("epic-core").unwrap().tier, Tier::Active);
|
|
93
|
+
assert_eq!(sub.by_id("feat-query").unwrap().tier, Tier::Active);
|
|
94
|
+
assert_eq!(sub.by_id("idea-future-board").unwrap().tier, Tier::Backlog);
|
|
95
|
+
assert_eq!(sub.by_id("release-v1.0").unwrap().tier, Tier::Releases);
|
|
96
|
+
assert_eq!(sub.by_id("feat-old").unwrap().tier, Tier::Archive);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn golden_backlog_item_minimal_no_validation_warning() {
|
|
101
|
+
let (sub, report) = Substrate::load(golden_root()).unwrap();
|
|
102
|
+
|
|
103
|
+
let backlog = sub.by_id("idea-future-board").unwrap();
|
|
104
|
+
assert_eq!(backlog.tier, Tier::Backlog);
|
|
105
|
+
assert!(backlog.kind.is_none());
|
|
106
|
+
assert!(backlog.stage.is_none());
|
|
107
|
+
assert_eq!(backlog.tags, vec!["tooling"]);
|
|
108
|
+
|
|
109
|
+
// No validation warnings for this backlog item
|
|
110
|
+
let warnings: Vec<_> = report
|
|
111
|
+
.validation_warnings
|
|
112
|
+
.iter()
|
|
113
|
+
.filter(|d| d.id.as_deref() == Some("idea-future-board"))
|
|
114
|
+
.collect();
|
|
115
|
+
assert!(
|
|
116
|
+
warnings.is_empty(),
|
|
117
|
+
"backlog item should not trigger validation warnings"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[test]
|
|
122
|
+
fn golden_releases_item_is_terminal() {
|
|
123
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
124
|
+
let rel = sub.by_id("release-v1.0").unwrap();
|
|
125
|
+
assert_eq!(rel.tier, Tier::Releases);
|
|
126
|
+
assert!(rel.is_terminal());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn golden_archive_item_is_terminal() {
|
|
131
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
132
|
+
let archived = sub.by_id("feat-old").unwrap();
|
|
133
|
+
assert_eq!(archived.tier, Tier::Archive);
|
|
134
|
+
assert!(archived.is_terminal());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn golden_active_implementing_is_not_terminal() {
|
|
139
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
140
|
+
let feat = sub.by_id("feat-parser").unwrap();
|
|
141
|
+
assert_eq!(feat.tier, Tier::Active);
|
|
142
|
+
assert_eq!(feat.stage.as_deref(), Some("implementing"));
|
|
143
|
+
assert!(!feat.is_terminal());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn golden_load_order_is_byte_sorted() {
|
|
148
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
149
|
+
|
|
150
|
+
// All items should be in byte-sorted path order.
|
|
151
|
+
// Verify that paths are non-decreasing.
|
|
152
|
+
use std::os::unix::ffi::OsStrExt;
|
|
153
|
+
let paths: Vec<&std::path::Path> = sub.items().iter().map(|i| i.path.as_path()).collect();
|
|
154
|
+
for window in paths.windows(2) {
|
|
155
|
+
let a = window[0].as_os_str().as_bytes();
|
|
156
|
+
let b = window[1].as_os_str().as_bytes();
|
|
157
|
+
assert!(
|
|
158
|
+
a <= b,
|
|
159
|
+
"items not in byte-sorted order:\n {}\n {}",
|
|
160
|
+
window[0].display(),
|
|
161
|
+
window[1].display()
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#[test]
|
|
167
|
+
fn golden_rel_path_is_relative() {
|
|
168
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
169
|
+
for item in sub.items() {
|
|
170
|
+
assert!(
|
|
171
|
+
item.rel_path.is_relative(),
|
|
172
|
+
"rel_path should be relative, got: {}",
|
|
173
|
+
item.rel_path.display()
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn golden_raw_text_includes_frontmatter() {
|
|
180
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
181
|
+
let feat = sub.by_id("feat-parser").unwrap();
|
|
182
|
+
assert!(
|
|
183
|
+
feat.raw_text.contains("id: feat-parser"),
|
|
184
|
+
"raw_text must include frontmatter"
|
|
185
|
+
);
|
|
186
|
+
assert!(
|
|
187
|
+
feat.raw_text.contains("# Parser feature"),
|
|
188
|
+
"raw_text must include body"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[test]
|
|
193
|
+
fn golden_body_excludes_frontmatter() {
|
|
194
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
195
|
+
let feat = sub.by_id("feat-parser").unwrap();
|
|
196
|
+
assert!(
|
|
197
|
+
feat.body.contains("# Parser feature"),
|
|
198
|
+
"body must contain the heading"
|
|
199
|
+
);
|
|
200
|
+
assert!(
|
|
201
|
+
!feat.body.contains("id: feat-parser"),
|
|
202
|
+
"body must NOT contain frontmatter"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Dependency graph tests over the golden fixture ────────────────────────────
|
|
207
|
+
|
|
208
|
+
#[test]
|
|
209
|
+
fn golden_feat_query_deps_not_satisfied() {
|
|
210
|
+
// feat-query depends on feat-parser, which is at stage:implementing (not terminal)
|
|
211
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
212
|
+
let feat_query = sub.by_id("feat-query").unwrap();
|
|
213
|
+
assert!(
|
|
214
|
+
!sub.deps_satisfied(feat_query),
|
|
215
|
+
"feat-query deps should NOT be satisfied (feat-parser is implementing)"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn golden_feat_parser_deps_satisfied() {
|
|
221
|
+
// feat-parser has no dependencies
|
|
222
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
223
|
+
let feat_parser = sub.by_id("feat-parser").unwrap();
|
|
224
|
+
assert!(
|
|
225
|
+
sub.deps_satisfied(feat_parser),
|
|
226
|
+
"feat-parser has no deps, should be satisfied"
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn golden_unmet_deps_feat_query() {
|
|
232
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
233
|
+
let feat_query = sub.by_id("feat-query").unwrap();
|
|
234
|
+
let unmet = sub.unmet_deps(feat_query);
|
|
235
|
+
assert!(
|
|
236
|
+
unmet.contains(&"feat-parser"),
|
|
237
|
+
"feat-parser should be in unmet deps"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn golden_dependents_of_feat_parser() {
|
|
243
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
244
|
+
let deps_of = sub.dependents_of("feat-parser");
|
|
245
|
+
let ids: Vec<&str> = deps_of.iter().map(|i| i.id.as_str()).collect();
|
|
246
|
+
assert!(
|
|
247
|
+
ids.contains(&"feat-query"),
|
|
248
|
+
"feat-query depends on feat-parser"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn golden_children_of_epic_core() {
|
|
254
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
255
|
+
let children = sub.children_of("epic-core");
|
|
256
|
+
let ids: Vec<&str> = children.iter().map(|i| i.id.as_str()).collect();
|
|
257
|
+
assert!(ids.contains(&"feat-parser"));
|
|
258
|
+
assert!(ids.contains(&"feat-query"));
|
|
259
|
+
assert!(ids.contains(&"story-security-gate"));
|
|
260
|
+
// story-parse-unit's parent is feat-parser, not epic-core
|
|
261
|
+
assert!(!ids.contains(&"story-parse-unit"));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[test]
|
|
265
|
+
fn golden_children_of_feat_parser() {
|
|
266
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
267
|
+
let children = sub.children_of("feat-parser");
|
|
268
|
+
let ids: Vec<&str> = children.iter().map(|i| i.id.as_str()).collect();
|
|
269
|
+
assert!(ids.contains(&"story-parse-unit"));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Filter tests over the golden fixture ──────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
fn golden_filter_stage_implementing() {
|
|
276
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
277
|
+
let f = Filter {
|
|
278
|
+
stage: Match::Equals("implementing".into()),
|
|
279
|
+
..Filter::default()
|
|
280
|
+
};
|
|
281
|
+
let results = sub.query(&f);
|
|
282
|
+
let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
|
|
283
|
+
assert!(ids.contains(&"epic-core"));
|
|
284
|
+
assert!(ids.contains(&"feat-parser"));
|
|
285
|
+
assert!(ids.contains(&"story-parse-unit"));
|
|
286
|
+
assert!(ids.contains(&"story-security-gate"));
|
|
287
|
+
assert!(!ids.contains(&"feat-query"), "feat-query is at review");
|
|
288
|
+
assert!(
|
|
289
|
+
!ids.contains(&"idea-future-board"),
|
|
290
|
+
"backlog item has no stage"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[test]
|
|
295
|
+
fn golden_filter_kind_feature() {
|
|
296
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
297
|
+
let f = Filter {
|
|
298
|
+
kind: Match::Equals("feature".into()),
|
|
299
|
+
..Filter::default()
|
|
300
|
+
};
|
|
301
|
+
let results = sub.query(&f);
|
|
302
|
+
// feat-parser (active), feat-query (active), feat-old (archive) all have kind:feature
|
|
303
|
+
// But feat-parser also appears in archive as a duplicate — both copies have kind:feature
|
|
304
|
+
// and both stay in items(). So we expect at least feat-parser(active) + feat-query + feat-old.
|
|
305
|
+
let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
|
|
306
|
+
assert!(ids.contains(&"feat-parser"));
|
|
307
|
+
assert!(ids.contains(&"feat-query"));
|
|
308
|
+
assert!(ids.contains(&"feat-old"));
|
|
309
|
+
assert!(!ids.contains(&"epic-core"));
|
|
310
|
+
assert!(!ids.contains(&"story-parse-unit"));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[test]
|
|
314
|
+
fn golden_filter_tag_tooling() {
|
|
315
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
316
|
+
let f = Filter {
|
|
317
|
+
tags: vec!["tooling".into()],
|
|
318
|
+
..Filter::default()
|
|
319
|
+
};
|
|
320
|
+
let results = sub.query(&f);
|
|
321
|
+
let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
|
|
322
|
+
assert!(ids.contains(&"epic-core"));
|
|
323
|
+
assert!(ids.contains(&"feat-parser"));
|
|
324
|
+
assert!(
|
|
325
|
+
!ids.contains(&"story-security-gate"),
|
|
326
|
+
"security gate has only [security]"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[test]
|
|
331
|
+
fn golden_filter_tag_tooling_and_perf() {
|
|
332
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
333
|
+
// feat-query has both tooling and perf tags
|
|
334
|
+
let f = Filter {
|
|
335
|
+
tags: vec!["tooling".into(), "perf".into()],
|
|
336
|
+
..Filter::default()
|
|
337
|
+
};
|
|
338
|
+
let results = sub.query(&f);
|
|
339
|
+
let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
|
|
340
|
+
assert!(ids.contains(&"feat-query"));
|
|
341
|
+
assert!(
|
|
342
|
+
!ids.contains(&"feat-parser"),
|
|
343
|
+
"feat-parser only has [tooling]"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#[test]
|
|
348
|
+
fn golden_filter_blocking() {
|
|
349
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
350
|
+
// feat-query depends on feat-parser; so feat-query is a "dependent" of feat-parser
|
|
351
|
+
let f = Filter {
|
|
352
|
+
blocking: Some("feat-parser".into()),
|
|
353
|
+
..Filter::default()
|
|
354
|
+
};
|
|
355
|
+
let results = sub.query(&f);
|
|
356
|
+
let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
|
|
357
|
+
assert!(ids.contains(&"feat-query"));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[test]
|
|
361
|
+
fn golden_filter_parent_epic_core() {
|
|
362
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
363
|
+
let f = Filter {
|
|
364
|
+
parent: Match::Equals("epic-core".into()),
|
|
365
|
+
..Filter::default()
|
|
366
|
+
};
|
|
367
|
+
let results = sub.query(&f);
|
|
368
|
+
let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
|
|
369
|
+
assert!(ids.contains(&"feat-parser"));
|
|
370
|
+
assert!(ids.contains(&"feat-query"));
|
|
371
|
+
assert!(ids.contains(&"story-security-gate"));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[test]
|
|
375
|
+
fn golden_filter_gate_security() {
|
|
376
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
377
|
+
let f = Filter {
|
|
378
|
+
gate: Match::Equals("security".into()),
|
|
379
|
+
..Filter::default()
|
|
380
|
+
};
|
|
381
|
+
let results = sub.query(&f);
|
|
382
|
+
assert_eq!(results.len(), 1);
|
|
383
|
+
assert_eq!(results[0].id, "story-security-gate");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#[test]
|
|
387
|
+
fn golden_filter_release_binding() {
|
|
388
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
389
|
+
let f = Filter {
|
|
390
|
+
release: Match::Equals("v1.0.0".into()),
|
|
391
|
+
..Filter::default()
|
|
392
|
+
};
|
|
393
|
+
let results = sub.query(&f);
|
|
394
|
+
let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
|
|
395
|
+
assert!(ids.contains(&"feat-parser"));
|
|
396
|
+
assert!(ids.contains(&"feat-query"));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn golden_filter_release_is_null() {
|
|
401
|
+
let (sub, _) = Substrate::load(golden_root()).unwrap();
|
|
402
|
+
let f = Filter {
|
|
403
|
+
release: Match::IsNull,
|
|
404
|
+
..Filter::default()
|
|
405
|
+
};
|
|
406
|
+
let results = sub.query(&f);
|
|
407
|
+
// Items without a release_binding: epic-core, story-parse-unit, story-security-gate,
|
|
408
|
+
// idea-future-board, release-v1.0, feat-old, and the archive copy of feat-parser
|
|
409
|
+
let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
|
|
410
|
+
assert!(
|
|
411
|
+
!ids.contains(&"feat-query"),
|
|
412
|
+
"feat-query has release_binding: v1.0.0"
|
|
413
|
+
);
|
|
414
|
+
// feat-parser active copy has release_binding v1.0.0
|
|
415
|
+
// Find if feat-parser is in there — it should not be (active copy has binding)
|
|
416
|
+
// But archive copy has null — however by_id won't help here; query returns ALL items
|
|
417
|
+
// including both copies of feat-parser.
|
|
418
|
+
// The active copy of feat-parser has release_binding: v1.0.0, so it should NOT match.
|
|
419
|
+
// The archive copy has null, so it SHOULD match.
|
|
420
|
+
// Both copies are in items().
|
|
421
|
+
let feat_parser_matches: Vec<_> = results.iter().filter(|i| i.id == "feat-parser").collect();
|
|
422
|
+
// Only the archive copy (with null release_binding) should match
|
|
423
|
+
assert_eq!(
|
|
424
|
+
feat_parser_matches.len(),
|
|
425
|
+
1,
|
|
426
|
+
"only the archive copy of feat-parser (null release_binding) should match"
|
|
427
|
+
);
|
|
428
|
+
assert_eq!(feat_parser_matches[0].tier, Tier::Archive);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Duplicate-id precedence: releases vs archive (load order disagrees) ───────
|
|
432
|
+
|
|
433
|
+
#[test]
|
|
434
|
+
fn precedence_releases_wins_over_archive_despite_load_order() {
|
|
435
|
+
// `dup-item` exists in BOTH releases/ and archive/. In byte-sorted load
|
|
436
|
+
// order `.work/archive/...` sorts BEFORE `.work/releases/...` ('a' < 'r'),
|
|
437
|
+
// so the archive copy is encountered FIRST. Tier precedence (releases=1 >
|
|
438
|
+
// archive=2) must still win — proving `by_id` resolves by precedence, NOT
|
|
439
|
+
// by load order. This is the single pairing the golden fixture can't prove
|
|
440
|
+
// (active-vs-archive, where load order and precedence happen to agree).
|
|
441
|
+
let (sub, report) = Substrate::load(precedence_root()).unwrap();
|
|
442
|
+
|
|
443
|
+
// Sanity: both copies stay in items(), and the archive copy really does
|
|
444
|
+
// sort before the releases copy in load order.
|
|
445
|
+
use std::os::unix::ffi::OsStrExt;
|
|
446
|
+
let order: Vec<&std::path::Path> = sub
|
|
447
|
+
.items()
|
|
448
|
+
.iter()
|
|
449
|
+
.filter(|i| i.id == "dup-item")
|
|
450
|
+
.map(|i| i.path.as_path())
|
|
451
|
+
.collect();
|
|
452
|
+
assert_eq!(order.len(), 2, "both copies must remain in items()");
|
|
453
|
+
assert!(
|
|
454
|
+
order[0].as_os_str().as_bytes() < order[1].as_os_str().as_bytes(),
|
|
455
|
+
"load order must be byte-sorted"
|
|
456
|
+
);
|
|
457
|
+
assert!(
|
|
458
|
+
order[0].to_str().unwrap().contains("/archive/"),
|
|
459
|
+
"the archive copy must be FIRST in load order (it sorts before releases)"
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// by_id must return the RELEASES copy (precedence 1), not the
|
|
463
|
+
// load-order-first archive copy (precedence 2).
|
|
464
|
+
let canonical = sub.by_id("dup-item").unwrap();
|
|
465
|
+
assert_eq!(
|
|
466
|
+
canonical.tier,
|
|
467
|
+
Tier::Releases,
|
|
468
|
+
"releases (precedence 1) must win over archive (precedence 2), even \
|
|
469
|
+
though archive sorts first in load order"
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
// The duplicate diagnostic is filed against the non-canonical (archive) copy.
|
|
473
|
+
let dups: Vec<_> = report
|
|
474
|
+
.duplicate_ids
|
|
475
|
+
.iter()
|
|
476
|
+
.filter(|d| d.id.as_deref() == Some("dup-item"))
|
|
477
|
+
.collect();
|
|
478
|
+
assert_eq!(
|
|
479
|
+
dups.len(),
|
|
480
|
+
1,
|
|
481
|
+
"exactly one duplicate diagnostic, for the non-canonical copy"
|
|
482
|
+
);
|
|
483
|
+
assert_eq!(
|
|
484
|
+
dups[0].tier,
|
|
485
|
+
Tier::Archive,
|
|
486
|
+
"the diagnostic must point at the archive (loser) copy"
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── find_substrate_root integration ──────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
#[test]
|
|
493
|
+
fn find_root_finds_golden_fixture() {
|
|
494
|
+
let found = find_substrate_root(golden_root()).unwrap();
|
|
495
|
+
assert_eq!(
|
|
496
|
+
found.canonicalize().unwrap(),
|
|
497
|
+
golden_root().canonicalize().unwrap()
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
#[test]
|
|
502
|
+
fn find_root_finds_from_subdirectory() {
|
|
503
|
+
let subdir = golden_root().join(".work/active/features");
|
|
504
|
+
let found = find_substrate_root(&subdir).unwrap();
|
|
505
|
+
assert_eq!(
|
|
506
|
+
found.canonicalize().unwrap(),
|
|
507
|
+
golden_root().canonicalize().unwrap()
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#[test]
|
|
512
|
+
fn find_root_returns_none_outside_fixture() {
|
|
513
|
+
// /tmp has no .work/CONVENTIONS.md ancestor
|
|
514
|
+
let result = find_substrate_root(Path::new("/tmp"));
|
|
515
|
+
assert!(result.is_none());
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ── Validation warnings integration ──────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
#[test]
|
|
521
|
+
fn golden_validation_warns_on_malformed_active_items() {
|
|
522
|
+
// The golden fixture has the malformed item (no id) which becomes a parse error,
|
|
523
|
+
// not a validation warning. Let's verify: only the archive/backlog items lack
|
|
524
|
+
// kind/stage without triggering validation warnings.
|
|
525
|
+
let (sub, report) = Substrate::load(golden_root()).unwrap();
|
|
526
|
+
|
|
527
|
+
// No active items should trigger validation warnings in the golden fixture
|
|
528
|
+
// (all active items have kind + stage; the malformed one was excluded as parse error)
|
|
529
|
+
let active_warnings: Vec<_> = report
|
|
530
|
+
.validation_warnings
|
|
531
|
+
.iter()
|
|
532
|
+
.filter(|d| d.tier == Tier::Active)
|
|
533
|
+
.collect();
|
|
534
|
+
assert!(
|
|
535
|
+
active_warnings.is_empty(),
|
|
536
|
+
"no active items should have validation warnings in golden fixture, got: {active_warnings:?}"
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// sub is used to confirm items loaded
|
|
540
|
+
assert!(sub.by_id("epic-core").is_some());
|
|
541
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*/work-view binary -diff
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# work-view prebuilt binaries
|
|
2
|
+
|
|
3
|
+
This directory contains platform-matched prebuilt binaries of `work-view`,
|
|
4
|
+
organised by target triple:
|
|
5
|
+
|
|
6
|
+
| Triple | Platform |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `x86_64-unknown-linux-musl` | Linux x86_64 (static) |
|
|
9
|
+
| `aarch64-unknown-linux-musl` | Linux aarch64 / ARM64 (static) |
|
|
10
|
+
| `x86_64-apple-darwin` | macOS Intel |
|
|
11
|
+
| `aarch64-apple-darwin` | macOS Apple Silicon (M1/M2/M3) |
|
|
12
|
+
|
|
13
|
+
## How binaries are produced
|
|
14
|
+
|
|
15
|
+
Binaries are built and committed by the CI workflow at
|
|
16
|
+
`.github/workflows/build-work-view.yml`. The workflow has two modes:
|
|
17
|
+
|
|
18
|
+
- **PR / push** — builds all four targets, uploads as GitHub Actions artifacts,
|
|
19
|
+
and runs a size guard (fails if any binary exceeds 8 MB). Does NOT commit
|
|
20
|
+
binaries to the tree.
|
|
21
|
+
- **Manual refresh (`workflow_dispatch`)** — builds all four targets and commits
|
|
22
|
+
the binaries under `dist/<triple>/work-view`. Run this job after
|
|
23
|
+
`./scripts/bump-version.sh agile-workflow <level>` has committed and pushed
|
|
24
|
+
the new source stamp; a pre-bump refresh compiles the old version into the
|
|
25
|
+
binaries.
|
|
26
|
+
|
|
27
|
+
## Do not hand-edit
|
|
28
|
+
|
|
29
|
+
Binary files in this directory are generated artifacts. Do not add, replace, or
|
|
30
|
+
modify them by hand. Changes made outside the CI refresh job may be silently
|
|
31
|
+
overwritten and will not be reproducible.
|
|
32
|
+
|
|
33
|
+
## Fallback behaviour
|
|
34
|
+
|
|
35
|
+
`install-work-view.sh` installs a matching prebuilt binary for supported
|
|
36
|
+
platforms. If that binary is missing, fails its smoke test, or reports a stale
|
|
37
|
+
version, installation fails loudly because the interactive board depends on the
|
|
38
|
+
compiled binary.
|
|
39
|
+
|
|
40
|
+
Only unsupported platforms fall back to `scripts/work-view.sh` (the pure-bash
|
|
41
|
+
CLI implementation). That fallback keeps agent query workflows working, but it
|
|
42
|
+
does not support `work-view board`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|