@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,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`.