@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,162 @@
1
+ //! Embedded static assets for `work-view board`.
2
+
3
+ pub(crate) struct Asset {
4
+ pub bytes: &'static [u8],
5
+ pub content_type: &'static str,
6
+ }
7
+
8
+ const INDEX_HTML: &[u8] = include_bytes!("assets/index.html");
9
+ const TOKENS_CSS: &[u8] = include_bytes!("assets/tokens.css");
10
+ const COMPONENTS_CSS: &[u8] = include_bytes!("assets/components.css");
11
+ const MOTION_CSS: &[u8] = include_bytes!("assets/motion.css");
12
+ const BOARD_CSS: &[u8] = include_bytes!("assets/board.css");
13
+ const BOARD_JS: &[u8] = include_bytes!("assets/board.js");
14
+ const STATE_JS: &[u8] = include_bytes!("assets/state.js");
15
+ const FILTERS_JS: &[u8] = include_bytes!("assets/filters.js");
16
+ const MARKDOWN_JS: &[u8] = include_bytes!("assets/markdown.js");
17
+ const CARD_JS: &[u8] = include_bytes!("assets/card.js");
18
+ const DETAIL_JS: &[u8] = include_bytes!("assets/detail.js");
19
+ const VIEWS_JS: &[u8] = include_bytes!("assets/views.js");
20
+ const KANBAN_JS: &[u8] = include_bytes!("assets/kanban.js");
21
+ const DEPENDENCY_JS: &[u8] = include_bytes!("assets/dependency.js");
22
+ const TABLE_JS: &[u8] = include_bytes!("assets/table.js");
23
+
24
+ pub(crate) fn asset_for_path(path: &str) -> Option<Asset> {
25
+ match path {
26
+ "/" | "/index.html" => Some(Asset {
27
+ bytes: INDEX_HTML,
28
+ content_type: "text/html; charset=utf-8",
29
+ }),
30
+ "/assets/tokens.css" => Some(Asset {
31
+ bytes: TOKENS_CSS,
32
+ content_type: "text/css; charset=utf-8",
33
+ }),
34
+ "/assets/components.css" => Some(Asset {
35
+ bytes: COMPONENTS_CSS,
36
+ content_type: "text/css; charset=utf-8",
37
+ }),
38
+ "/assets/motion.css" => Some(Asset {
39
+ bytes: MOTION_CSS,
40
+ content_type: "text/css; charset=utf-8",
41
+ }),
42
+ "/assets/board.css" => Some(Asset {
43
+ bytes: BOARD_CSS,
44
+ content_type: "text/css; charset=utf-8",
45
+ }),
46
+ "/assets/board.js" => Some(Asset {
47
+ bytes: BOARD_JS,
48
+ content_type: "text/javascript; charset=utf-8",
49
+ }),
50
+ "/assets/state.js" => Some(Asset {
51
+ bytes: STATE_JS,
52
+ content_type: "text/javascript; charset=utf-8",
53
+ }),
54
+ "/assets/filters.js" => Some(Asset {
55
+ bytes: FILTERS_JS,
56
+ content_type: "text/javascript; charset=utf-8",
57
+ }),
58
+ "/assets/markdown.js" => Some(Asset {
59
+ bytes: MARKDOWN_JS,
60
+ content_type: "text/javascript; charset=utf-8",
61
+ }),
62
+ "/assets/card.js" => Some(Asset {
63
+ bytes: CARD_JS,
64
+ content_type: "text/javascript; charset=utf-8",
65
+ }),
66
+ "/assets/detail.js" => Some(Asset {
67
+ bytes: DETAIL_JS,
68
+ content_type: "text/javascript; charset=utf-8",
69
+ }),
70
+ "/assets/views.js" => Some(Asset {
71
+ bytes: VIEWS_JS,
72
+ content_type: "text/javascript; charset=utf-8",
73
+ }),
74
+ "/assets/kanban.js" => Some(Asset {
75
+ bytes: KANBAN_JS,
76
+ content_type: "text/javascript; charset=utf-8",
77
+ }),
78
+ "/assets/dependency.js" => Some(Asset {
79
+ bytes: DEPENDENCY_JS,
80
+ content_type: "text/javascript; charset=utf-8",
81
+ }),
82
+ "/assets/table.js" => Some(Asset {
83
+ bytes: TABLE_JS,
84
+ content_type: "text/javascript; charset=utf-8",
85
+ }),
86
+ _ => None,
87
+ }
88
+ }
89
+
90
+ #[cfg(test)]
91
+ mod tests {
92
+ use super::*;
93
+
94
+ #[test]
95
+ fn root_and_index_return_html() {
96
+ for path in ["/", "/index.html"] {
97
+ let asset = asset_for_path(path).expect("expected index asset");
98
+ assert_eq!(asset.content_type, "text/html; charset=utf-8");
99
+ assert!(asset.bytes.starts_with(b"<!doctype html>"));
100
+ }
101
+ }
102
+
103
+ #[test]
104
+ fn css_and_js_return_expected_types() {
105
+ for path in [
106
+ "/assets/tokens.css",
107
+ "/assets/components.css",
108
+ "/assets/motion.css",
109
+ "/assets/board.css",
110
+ ] {
111
+ let css = asset_for_path(path).expect("expected CSS asset");
112
+ assert_eq!(css.content_type, "text/css; charset=utf-8");
113
+ assert!(!css.bytes.is_empty());
114
+ }
115
+ for path in [
116
+ "/assets/board.js",
117
+ "/assets/state.js",
118
+ "/assets/filters.js",
119
+ "/assets/markdown.js",
120
+ "/assets/card.js",
121
+ "/assets/detail.js",
122
+ "/assets/views.js",
123
+ "/assets/kanban.js",
124
+ "/assets/dependency.js",
125
+ "/assets/table.js",
126
+ ] {
127
+ let js = asset_for_path(path).expect("expected JS asset");
128
+ assert_eq!(js.content_type, "text/javascript; charset=utf-8");
129
+ assert!(!js.bytes.is_empty());
130
+ }
131
+ }
132
+
133
+ #[test]
134
+ fn shipped_css_has_no_remote_dependencies() {
135
+ for path in [
136
+ "/assets/tokens.css",
137
+ "/assets/components.css",
138
+ "/assets/motion.css",
139
+ "/assets/board.css",
140
+ ] {
141
+ let css = asset_for_path(path).expect("expected CSS asset");
142
+ let body = std::str::from_utf8(css.bytes).expect("CSS should be UTF-8");
143
+ assert!(
144
+ !body.contains("@import url"),
145
+ "{path} must not import remote CSS"
146
+ );
147
+ assert!(
148
+ !body.contains("fonts.googleapis"),
149
+ "{path} must not reference Google Fonts"
150
+ );
151
+ assert!(
152
+ !body.contains("https://"),
153
+ "{path} must not reference remote HTTPS assets"
154
+ );
155
+ }
156
+ }
157
+
158
+ #[test]
159
+ fn missing_asset_returns_none() {
160
+ assert!(asset_for_path("/assets/missing.js").is_none());
161
+ }
162
+ }
@@ -0,0 +1,204 @@
1
+ //! JSON substrate feed for the local board host.
2
+
3
+ use std::fmt;
4
+ use std::path::Path;
5
+
6
+ use serde::Serialize;
7
+ use work_view_core::error::LoadError;
8
+ use work_view_core::index::{Diagnostic, LoadReport, Substrate};
9
+ use work_view_core::model::{Item, Tier};
10
+
11
+ use crate::actionable::dependency_status;
12
+ use crate::args::WORK_VIEW_VERSION;
13
+
14
+ #[derive(Debug)]
15
+ pub(crate) enum FeedError {
16
+ Load(LoadError),
17
+ Json(serde_json::Error),
18
+ }
19
+
20
+ impl fmt::Display for FeedError {
21
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22
+ match self {
23
+ FeedError::Load(e) => write!(f, "could not load substrate: {e}"),
24
+ FeedError::Json(e) => write!(f, "could not serialize substrate feed: {e}"),
25
+ }
26
+ }
27
+ }
28
+
29
+ #[derive(Serialize)]
30
+ struct FeedSnapshot {
31
+ work_view_version: &'static str,
32
+ project: String,
33
+ root_rel: &'static str,
34
+ items: Vec<FeedItem>,
35
+ diagnostics: FeedDiagnostics,
36
+ }
37
+
38
+ #[derive(Serialize)]
39
+ struct FeedItem {
40
+ id: String,
41
+ kind: Option<String>,
42
+ stage: Option<String>,
43
+ tags: Vec<String>,
44
+ parent: Option<String>,
45
+ depends_on: Vec<String>,
46
+ release_binding: Option<String>,
47
+ gate_origin: Option<String>,
48
+ research_origin: Option<String>,
49
+ scan_origin: Option<String>,
50
+ research_refs: Vec<String>,
51
+ created: Option<String>,
52
+ updated: Option<String>,
53
+ tier: &'static str,
54
+ rel_path: String,
55
+ body: String,
56
+ is_terminal: bool,
57
+ ready: bool,
58
+ blocked: bool,
59
+ unmet_deps: Vec<String>,
60
+ dependents: Vec<String>,
61
+ children: Vec<String>,
62
+ }
63
+
64
+ #[derive(Serialize)]
65
+ struct FeedDiagnostics {
66
+ parse_errors: Vec<ParseDiagnostic>,
67
+ validation_warnings: Vec<ItemDiagnostic>,
68
+ duplicate_ids: Vec<ItemDiagnostic>,
69
+ }
70
+
71
+ #[derive(Serialize)]
72
+ struct ParseDiagnostic {
73
+ rel_path: String,
74
+ reason: String,
75
+ }
76
+
77
+ #[derive(Serialize)]
78
+ struct ItemDiagnostic {
79
+ rel_path: String,
80
+ tier: &'static str,
81
+ id: Option<String>,
82
+ field: Option<String>,
83
+ reason: String,
84
+ }
85
+
86
+ pub(crate) fn build_feed(root: &Path) -> Result<String, FeedError> {
87
+ let (sub, report) = Substrate::load(root).map_err(FeedError::Load)?;
88
+ let snapshot = FeedSnapshot {
89
+ work_view_version: WORK_VIEW_VERSION,
90
+ project: project_name(root),
91
+ root_rel: ".",
92
+ items: sub
93
+ .items()
94
+ .iter()
95
+ .map(|item| feed_item(&sub, item))
96
+ .collect(),
97
+ diagnostics: feed_diagnostics(root, report),
98
+ };
99
+
100
+ serde_json::to_string(&snapshot).map_err(FeedError::Json)
101
+ }
102
+
103
+ fn feed_item(sub: &Substrate, item: &Item) -> FeedItem {
104
+ let (ready, blocked) = dependency_status(sub, item);
105
+ FeedItem {
106
+ id: item.id.clone(),
107
+ kind: item.kind.clone(),
108
+ stage: item.stage.clone(),
109
+ tags: item.tags.clone(),
110
+ parent: item.parent.clone(),
111
+ depends_on: item.depends_on.clone(),
112
+ release_binding: item.release_binding.clone(),
113
+ gate_origin: item.gate_origin.clone(),
114
+ research_origin: item.research_origin.clone(),
115
+ scan_origin: item.scan_origin.clone(),
116
+ research_refs: item.research_refs.clone(),
117
+ created: item.created.clone(),
118
+ updated: item.updated.clone(),
119
+ tier: tier_name(item.tier),
120
+ rel_path: path_string(&item.rel_path),
121
+ body: item.body.clone(),
122
+ is_terminal: item.is_terminal(),
123
+ ready,
124
+ blocked,
125
+ unmet_deps: sub
126
+ .unmet_deps(item)
127
+ .into_iter()
128
+ .map(str::to_string)
129
+ .collect(),
130
+ dependents: sub
131
+ .dependents_of(&item.id)
132
+ .into_iter()
133
+ .map(|dependent| dependent.id.clone())
134
+ .collect(),
135
+ children: sub
136
+ .children_of(&item.id)
137
+ .into_iter()
138
+ .map(|child| child.id.clone())
139
+ .collect(),
140
+ }
141
+ }
142
+
143
+ fn feed_diagnostics(root: &Path, report: LoadReport) -> FeedDiagnostics {
144
+ FeedDiagnostics {
145
+ parse_errors: report
146
+ .parse_errors
147
+ .into_iter()
148
+ .map(|error| ParseDiagnostic {
149
+ rel_path: rel_path(root, &error.path),
150
+ reason: error.reason,
151
+ })
152
+ .collect(),
153
+ validation_warnings: report
154
+ .validation_warnings
155
+ .into_iter()
156
+ .map(|diagnostic| item_diagnostic(root, diagnostic))
157
+ .collect(),
158
+ duplicate_ids: report
159
+ .duplicate_ids
160
+ .into_iter()
161
+ .map(|diagnostic| item_diagnostic(root, diagnostic))
162
+ .collect(),
163
+ }
164
+ }
165
+
166
+ fn item_diagnostic(root: &Path, diagnostic: Diagnostic) -> ItemDiagnostic {
167
+ ItemDiagnostic {
168
+ rel_path: rel_path(root, &diagnostic.path),
169
+ tier: tier_name(diagnostic.tier),
170
+ id: diagnostic.id,
171
+ field: diagnostic.field,
172
+ reason: diagnostic.reason,
173
+ }
174
+ }
175
+
176
+ fn rel_path(root: &Path, path: &Path) -> String {
177
+ match path.strip_prefix(root) {
178
+ Ok(path) => path_string(path),
179
+ Err(_) => path_string(path),
180
+ }
181
+ }
182
+
183
+ fn path_string(path: &Path) -> String {
184
+ path.components()
185
+ .map(|component| component.as_os_str().to_string_lossy())
186
+ .collect::<Vec<_>>()
187
+ .join("/")
188
+ }
189
+
190
+ fn tier_name(tier: Tier) -> &'static str {
191
+ match tier {
192
+ Tier::Active => "active",
193
+ Tier::Backlog => "backlog",
194
+ Tier::Releases => "releases",
195
+ Tier::Archive => "archive",
196
+ }
197
+ }
198
+
199
+ fn project_name(root: &Path) -> String {
200
+ root.file_name()
201
+ .map(|name| name.to_string_lossy().into_owned())
202
+ .filter(|name| !name.is_empty())
203
+ .unwrap_or_else(|| "substrate".to_string())
204
+ }
@@ -0,0 +1,246 @@
1
+ //! Board subcommand parsing and dispatch.
2
+
3
+ use std::env;
4
+
5
+ use work_view_core::index::find_substrate_root;
6
+
7
+ use crate::args::UsageError;
8
+
9
+ mod assets;
10
+ mod feed;
11
+ mod open;
12
+ mod server;
13
+
14
+ pub(crate) const BOARD_HELP: &str = "\
15
+ work-view board - serve the agile-workflow substrate board
16
+
17
+ Usage: work-view board [OPTIONS]
18
+ work-view serve [OPTIONS]
19
+
20
+ Options:
21
+ --port <n> Port to bind on localhost (default: 8181)
22
+ --no-open Do not open a browser after starting
23
+ --print Alias for --no-open
24
+ --once Serve one request, then exit (internal test mode)
25
+ --help, -h Show this help and exit\
26
+ ";
27
+
28
+ #[derive(Debug, PartialEq)]
29
+ pub(crate) struct BoardOptions {
30
+ pub port: u16,
31
+ pub no_open: bool,
32
+ pub once: bool,
33
+ }
34
+
35
+ impl Default for BoardOptions {
36
+ fn default() -> Self {
37
+ Self {
38
+ port: 8181,
39
+ no_open: false,
40
+ once: false,
41
+ }
42
+ }
43
+ }
44
+
45
+ #[derive(Debug, PartialEq)]
46
+ pub(crate) enum BoardParseOutcome {
47
+ Help,
48
+ Run(BoardOptions),
49
+ }
50
+
51
+ fn next_value<I: Iterator<Item = String>>(
52
+ flag: &str,
53
+ iter: &mut std::iter::Peekable<I>,
54
+ ) -> Result<String, UsageError> {
55
+ match iter.peek() {
56
+ None => Err(UsageError(format!("missing value for {flag}"))),
57
+ Some(next) if next.starts_with('-') => Err(UsageError(format!("missing value for {flag}"))),
58
+ Some(_) => Ok(iter.next().expect("peeked Some")),
59
+ }
60
+ }
61
+
62
+ pub(crate) fn parse_board_args<I: Iterator<Item = String>>(
63
+ args: I,
64
+ ) -> Result<BoardParseOutcome, UsageError> {
65
+ let mut opts = BoardOptions::default();
66
+ let mut iter = args.peekable();
67
+ let mut flags_done = false;
68
+
69
+ while let Some(arg) = iter.next() {
70
+ if flags_done {
71
+ return Err(UsageError(format!("unexpected argument: {arg}")));
72
+ }
73
+
74
+ match arg.as_str() {
75
+ "--" => {
76
+ flags_done = true;
77
+ }
78
+ "--help" | "-h" => {
79
+ return Ok(BoardParseOutcome::Help);
80
+ }
81
+ "--port" => {
82
+ let value = next_value("--port", &mut iter)?;
83
+ opts.port = value
84
+ .parse::<u16>()
85
+ .map_err(|_| UsageError(format!("invalid value for --port: {value}")))?;
86
+ }
87
+ "--no-open" | "--print" => {
88
+ opts.no_open = true;
89
+ }
90
+ "--once" => {
91
+ opts.once = true;
92
+ }
93
+ flag if flag.starts_with('-') => {
94
+ return Err(UsageError(format!("unknown board flag: {flag}")));
95
+ }
96
+ positional => {
97
+ return Err(UsageError(format!("unexpected argument: {positional}")));
98
+ }
99
+ }
100
+ }
101
+
102
+ Ok(BoardParseOutcome::Run(opts))
103
+ }
104
+
105
+ pub(crate) fn run_board(opts: BoardOptions) -> u8 {
106
+ let cwd = match env::current_dir() {
107
+ Ok(d) => d,
108
+ Err(e) => {
109
+ eprintln!("work-view: cannot determine current directory: {e}");
110
+ return 3;
111
+ }
112
+ };
113
+
114
+ let root = match find_substrate_root(&cwd) {
115
+ Some(r) => r,
116
+ None => {
117
+ eprintln!("work-view: no substrate found (no .work/CONVENTIONS.md in CWD or ancestor)");
118
+ return 2;
119
+ }
120
+ };
121
+
122
+ match server::serve_board(root, opts) {
123
+ Ok(_) => 0,
124
+ Err(e) => {
125
+ eprintln!("work-view board: {e}");
126
+ 3
127
+ }
128
+ }
129
+ }
130
+
131
+ #[cfg(test)]
132
+ mod tests {
133
+ use super::*;
134
+
135
+ fn parse(args: &[&str]) -> Result<BoardParseOutcome, UsageError> {
136
+ parse_board_args(args.iter().map(|s| s.to_string()))
137
+ }
138
+
139
+ fn run(args: &[&str]) -> BoardOptions {
140
+ match parse(args).expect("expected Ok") {
141
+ BoardParseOutcome::Run(opts) => opts,
142
+ BoardParseOutcome::Help => panic!("expected Run, got Help"),
143
+ }
144
+ }
145
+
146
+ fn err(args: &[&str]) -> String {
147
+ match parse(args).expect_err("expected UsageError") {
148
+ UsageError(msg) => msg,
149
+ }
150
+ }
151
+
152
+ #[test]
153
+ fn no_args_produces_default_options() {
154
+ assert_eq!(run(&[]), BoardOptions::default());
155
+ }
156
+
157
+ #[test]
158
+ fn port_flag_sets_port() {
159
+ let opts = run(&["--port", "9000"]);
160
+ assert_eq!(opts.port, 9000);
161
+ }
162
+
163
+ #[test]
164
+ fn no_open_flag_disables_browser_open() {
165
+ let opts = run(&["--no-open"]);
166
+ assert!(opts.no_open);
167
+ }
168
+
169
+ #[test]
170
+ fn print_is_no_open_alias() {
171
+ let opts = run(&["--print"]);
172
+ assert!(opts.no_open);
173
+ }
174
+
175
+ #[test]
176
+ fn once_flag_sets_once() {
177
+ let opts = run(&["--once"]);
178
+ assert!(opts.once);
179
+ }
180
+
181
+ #[test]
182
+ fn help_long_returns_help_outcome() {
183
+ assert!(matches!(
184
+ parse(&["--help"]).unwrap(),
185
+ BoardParseOutcome::Help
186
+ ));
187
+ }
188
+
189
+ #[test]
190
+ fn help_short_returns_help_outcome() {
191
+ assert!(matches!(parse(&["-h"]).unwrap(), BoardParseOutcome::Help));
192
+ }
193
+
194
+ #[test]
195
+ fn combined_flags_parse() {
196
+ let opts = run(&["--port", "9090", "--print", "--once"]);
197
+ assert_eq!(
198
+ opts,
199
+ BoardOptions {
200
+ port: 9090,
201
+ no_open: true,
202
+ once: true,
203
+ }
204
+ );
205
+ }
206
+
207
+ #[test]
208
+ fn missing_port_value_is_usage_error() {
209
+ let msg = err(&["--port"]);
210
+ assert!(msg.contains("missing value"), "got: {msg}");
211
+ assert!(msg.contains("--port"), "got: {msg}");
212
+ }
213
+
214
+ #[test]
215
+ fn port_followed_by_flag_is_missing_value() {
216
+ let msg = err(&["--port", "--no-open"]);
217
+ assert!(msg.contains("missing value"), "got: {msg}");
218
+ assert!(msg.contains("--port"), "got: {msg}");
219
+ }
220
+
221
+ #[test]
222
+ fn invalid_port_is_usage_error() {
223
+ let msg = err(&["--port", "not-a-port"]);
224
+ assert!(msg.contains("invalid value"), "got: {msg}");
225
+ assert!(msg.contains("--port"), "got: {msg}");
226
+ }
227
+
228
+ #[test]
229
+ fn unknown_flag_is_usage_error() {
230
+ let msg = err(&["--frobulate"]);
231
+ assert!(msg.contains("unknown board flag"), "got: {msg}");
232
+ assert!(msg.contains("--frobulate"), "got: {msg}");
233
+ }
234
+
235
+ #[test]
236
+ fn positional_arg_is_usage_error() {
237
+ let msg = err(&["extra"]);
238
+ assert!(msg.contains("unexpected argument"), "got: {msg}");
239
+ }
240
+
241
+ #[test]
242
+ fn positional_after_dash_dash_is_usage_error() {
243
+ let msg = err(&["--", "extra"]);
244
+ assert!(msg.contains("unexpected argument"), "got: {msg}");
245
+ }
246
+ }