shapeup-sdlc 1.6.2

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 (135) hide show
  1. package/.claude/settings.local.example.json +20 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +18 -0
  4. package/.env.shapeup.example +14 -0
  5. package/AGENTS.md +133 -0
  6. package/LICENSE +21 -0
  7. package/README.md +362 -0
  8. package/SECURITY.md +72 -0
  9. package/bin/init.mjs +329 -0
  10. package/commands/build.md +14 -0
  11. package/commands/eval.md +15 -0
  12. package/commands/hammer.md +12 -0
  13. package/commands/orient.md +11 -0
  14. package/commands/qa.md +12 -0
  15. package/commands/retro.md +13 -0
  16. package/commands/scopes.md +14 -0
  17. package/commands/shape.md +12 -0
  18. package/commands/ship.md +53 -0
  19. package/commands/wire.md +11 -0
  20. package/hooks/anti-rationalization.mjs +244 -0
  21. package/hooks/compact-snapshot.mjs +47 -0
  22. package/hooks/gate-deadline.mjs +151 -0
  23. package/hooks/gate-intake.mjs +110 -0
  24. package/hooks/gate-l2.mjs +161 -0
  25. package/hooks/gate-zerowork.mjs +264 -0
  26. package/hooks/hooks.json +118 -0
  27. package/hooks/lib/decision.mjs +183 -0
  28. package/hooks/safety-spine.mjs +296 -0
  29. package/hooks/sandbox-guard.mjs +172 -0
  30. package/hooks/session-rehydrate.mjs +109 -0
  31. package/hooks/slop-cleaner.mjs +176 -0
  32. package/oracles/_shared.mjs +46 -0
  33. package/oracles/http-oracle.mjs +155 -0
  34. package/oracles/index.mjs +36 -0
  35. package/oracles/process-oracle.mjs +146 -0
  36. package/oracles/snapshot-oracle.mjs +119 -0
  37. package/oracles/test-oracle.mjs +138 -0
  38. package/package.json +49 -0
  39. package/skills/advisor-protocol/SKILL.md +171 -0
  40. package/skills/ba-pitch-analyzer/SKILL.md +175 -0
  41. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +84 -0
  42. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +65 -0
  43. package/skills/ba-pitch-analyzer/assets/templates/assess-report.tmpl.md +127 -0
  44. package/skills/ba-pitch-analyzer/assets/templates/contracts/be-service.contract.tmpl.md +62 -0
  45. package/skills/ba-pitch-analyzer/assets/templates/contracts/offline-storage.contract.tmpl.md +92 -0
  46. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +66 -0
  47. package/skills/ba-pitch-analyzer/assets/templates/cross-context/context-map.tmpl.md +64 -0
  48. package/skills/ba-pitch-analyzer/assets/templates/cross-context/event-choreography.tmpl.md +77 -0
  49. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +104 -0
  50. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +79 -0
  51. package/skills/ba-pitch-analyzer/assets/templates/domain-model.tmpl.md +79 -0
  52. package/skills/ba-pitch-analyzer/assets/templates/feedback.tmpl.md +87 -0
  53. package/skills/ba-pitch-analyzer/assets/templates/integration.tmpl.md +64 -0
  54. package/skills/ba-pitch-analyzer/assets/templates/run-state.tmpl.md +77 -0
  55. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +105 -0
  56. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +215 -0
  57. package/skills/ba-pitch-analyzer/assets/templates/task-board.tmpl.md +35 -0
  58. package/skills/ba-pitch-analyzer/assets/templates/task-spike.tmpl.md +86 -0
  59. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +84 -0
  60. package/skills/ba-pitch-analyzer/assets/templates/usecase.tmpl.md +105 -0
  61. package/skills/ba-pitch-analyzer/assets/templates/ux-behavior.tmpl.md +65 -0
  62. package/skills/ba-pitch-analyzer/references/contract-patterns.md +152 -0
  63. package/skills/ba-pitch-analyzer/references/ddd-patterns.md +173 -0
  64. package/skills/ba-pitch-analyzer/references/doc-schemas.md +275 -0
  65. package/skills/ba-pitch-analyzer/references/integration-analysis.md +152 -0
  66. package/skills/ba-pitch-analyzer/references/task-generation.md +623 -0
  67. package/skills/ba-pitch-analyzer/references/test-surface.md +102 -0
  68. package/skills/ba-pitch-analyzer/references/ux-behavior-patterns.md +269 -0
  69. package/skills/ba-pitch-analyzer/scripts/board-derive.mjs +241 -0
  70. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +239 -0
  71. package/skills/coach/SKILL.md +202 -0
  72. package/skills/orient/SKILL.md +262 -0
  73. package/skills/qa-edge-hunter/SKILL.md +369 -0
  74. package/skills/scope-architect/SKILL.md +111 -0
  75. package/skills/scope-hammer/SKILL.md +186 -0
  76. package/skills/shapeup/SKILL.md +403 -0
  77. package/skills/shapeup/resources/breadboard-reflection.md +88 -0
  78. package/skills/shapeup/resources/breadboarding.md +334 -0
  79. package/skills/shapeup/resources/context-compaction.md +240 -0
  80. package/skills/shapeup/resources/framing-doc.md +92 -0
  81. package/skills/shapeup/resources/kickoff-doc.md +95 -0
  82. package/skills/shapeup/resources/shaping.md +194 -0
  83. package/skills/shapeup/resources/spike.md +86 -0
  84. package/skills/solution-architect/SKILL.md +129 -0
  85. package/skills/spec-evaluator/README.md +93 -0
  86. package/skills/spec-evaluator/SKILL.md +213 -0
  87. package/skills/spec-evaluator/references/anti-leniency.md +50 -0
  88. package/skills/spec-evaluator/references/dimension-contract.md +126 -0
  89. package/skills/spec-evaluator/references/dimensions/_registry.md +40 -0
  90. package/skills/spec-evaluator/references/dimensions/completeness.md +102 -0
  91. package/skills/spec-evaluator/references/dimensions/integration.md +129 -0
  92. package/skills/spec-evaluator/references/dimensions/performance.md +48 -0
  93. package/skills/spec-evaluator/references/dimensions/security.md +60 -0
  94. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +134 -0
  95. package/skills/spec-evaluator/references/dimensions/tdd-surface.md +110 -0
  96. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +100 -0
  97. package/skills/spec-evaluator/references/probing.md +177 -0
  98. package/skills/spec-evaluator/references/report-schema.md +101 -0
  99. package/skills/spec-evaluator/references/verdict-ledger.md +92 -0
  100. package/skills/spec-evaluator/scripts/verdict-ledger.mjs +166 -0
  101. package/skills/task-executor/SKILL.md +194 -0
  102. package/skills/tech-lead/README.md +71 -0
  103. package/skills/tech-lead/SKILL.md +459 -0
  104. package/skills/tech-lead/references/delegation.md +254 -0
  105. package/skills/tech-lead/references/gates.md +379 -0
  106. package/skills/tech-lead/references/invocation.md +45 -0
  107. package/skills/tech-lead/references/ledger-schema.md +214 -0
  108. package/skills/tech-lead/references/round-protocol.md +184 -0
  109. package/skills/tech-lead/references/state-model.md +66 -0
  110. package/skills/tech-lead/references/tiny-lane.md +52 -0
  111. package/skills/tech-lead/schemas/domain.schema.json +2294 -0
  112. package/skills/tech-lead/schemas/gate-answers.schema.json +92 -0
  113. package/skills/tech-lead/schemas/work-order.schema.json +21 -0
  114. package/skills/tech-lead/schemas/work-result.schema.json +40 -0
  115. package/skills/tech-lead/scripts/aegis-digest.mjs +124 -0
  116. package/skills/tech-lead/scripts/budget-check.mjs +156 -0
  117. package/skills/tech-lead/scripts/compile-order.mjs +518 -0
  118. package/skills/tech-lead/scripts/fit-check.mjs +196 -0
  119. package/skills/tech-lead/scripts/gate-answers.mjs +338 -0
  120. package/skills/tech-lead/scripts/ingest-result.mjs +270 -0
  121. package/skills/tech-lead/scripts/init-run.mjs +326 -0
  122. package/skills/tech-lead/scripts/lib/argv.mjs +224 -0
  123. package/skills/tech-lead/scripts/lib/contract-md.mjs +481 -0
  124. package/skills/tech-lead/scripts/lib/is-main.mjs +82 -0
  125. package/skills/tech-lead/scripts/lib/paths.mjs +278 -0
  126. package/skills/tech-lead/scripts/lib/ratchet-tree.mjs +112 -0
  127. package/skills/tech-lead/scripts/run-snapshot.mjs +269 -0
  128. package/skills/tech-lead/scripts/ship-report.mjs +275 -0
  129. package/skills/tech-lead/scripts/stats.mjs +423 -0
  130. package/skills/tech-lead/scripts/t0-verify.mjs +470 -0
  131. package/skills/tech-lead/scripts/trace-lint.mjs +367 -0
  132. package/skills/tech-lead/scripts/validate-envelope.mjs +283 -0
  133. package/skills/translator/README.md +66 -0
  134. package/skills/translator/SKILL.md +258 -0
  135. package/skills/translator/references/preservation-rules.md +102 -0
@@ -0,0 +1,102 @@
1
+ # Test Surface — Derivation Rules (v2.9)
2
+
3
+ `## Test Surface` is a **derived section** on each UC. It is generated *mechanically* from
4
+ material that already exists in the spec — never invented. If a test idea cannot cite its
5
+ source row below, it does not belong here (it belongs to exploratory QA, post-build).
6
+
7
+ **Why it exists.** `spec-conformance` grades the AC that were written; it cannot expand
8
+ them. The Test Surface is the systematic expansion — boundary values, negative cases,
9
+ error-code coverage — done once at spec time so the evaluator (`test-surface-conformance`
10
+ dimension) can probe it. Division of labor settled at the QA meeting (2026-06-11):
11
+ derivable tests = BA + Evaluator (this file); exploratory edges = `/qa-edge-hunter`,
12
+ post-PASS, on the running app.
13
+
14
+ ---
15
+
16
+ ## Derivation table (exhaustive — these four sources, nothing else)
17
+
18
+ | # | Source (must exist in spec) | Mechanical rule → test entries |
19
+ |---|---|---|
20
+ | D1 | `## Invariants` `[INV-NN]` | One **violation-attempt** entry per invariant: construct the action that would break it, expect rejection/hold. `TS-INV-NN`. |
21
+ | D2 | `## Error Cases` table rows | One entry per error code: trigger the Condition, expect the Code + HTTP status. `TS-ERR-<CODE>`. |
22
+ | D3 | Contract Request shape (standard lens: `contracts/*.contract.md`; lite lens: UC `Input` interface) | Per required field: one **missing-field** entry + one **type-violation** entry. Per bounded field (length/range/enum): one **boundary** entry at each edge (min−1, min, max, max+1 where meaningful). `TS-REQ-<field>-<kind>`. |
23
+ | D4 | Pitch `No-gos` that touch this UC's actor/action | One **breach-probe** entry per relevant no-go: attempt the excluded behavior via the UC's surface, expect it blocked or absent (a hidden-but-reachable path = FAIL). `TS-NOGO-NN`. |
24
+
25
+ **Dedup rule:** if D2 already covers a D3 case (e.g. error table has `VALIDATION_400` for
26
+ missing field), emit ONE entry and cite both sources — never two entries for one probe.
27
+
28
+ **Anti-invention rule (hard):** no entry may introduce a behavior, limit, or field not
29
+ present in the four sources. Discovering "we should also test X" during derivation = a spec
30
+ gap → surface it at GATE 4 as a question (max 2 rule applies), do not silently add a test.
31
+
32
+ ---
33
+
34
+ ## Entry schema (rows under `## Test Surface` in the UC)
35
+
36
+ ```markdown
37
+ ## Test Surface
38
+ <!-- DERIVED — regenerate via a retrofit-surface order; do not hand-author rows here.
39
+ Source must cite D1–D4. Exploratory/edge tests live in QA's charters, not here. -->
40
+ | ID | Oracle | Probe | Expect | Source |
41
+ |---|---|---|---|---|
42
+ | TS-INV-01 | ui | Attempt withdrawal exceeding balance via UC input | Rejected `INSUFFICIENT_FUNDS`, balance unchanged | D1: INV-01 |
43
+ | TS-ERR-NOT_FOUND | http | Submit with non-existent aggregate id | 404 `NOT_FOUND` per Error Cases | D2 |
44
+ | TS-REQ-amount-missing | http | Omit `amount` from request | 400 validation error, no side effect | D3 + D2 (dedup) |
45
+ | TS-REQ-amount-boundary | http | `amount` = 0 / 0.01 / max / max+0.01 | Per contract bounds: edges accepted, outside rejected | D3 |
46
+ | TS-NOGO-02 | ui | Attempt export via direct URL (pitch no-go: "no export") | Blocked/absent — no file served | D4 |
47
+ ```
48
+
49
+ `Probe` must be executable against the running deliverable — no "verify code does X" static
50
+ phrasing.
51
+
52
+ ### The `Oracle` column (evaluation-contract tag)
53
+
54
+ `Oracle` declares **how the evaluator verifies the row** — it is the dispatch key of the
55
+ evaluation contract. One verdict per row, single judge; the oracle changes only *how* evidence is
56
+ gathered, never *who* decides.
57
+
58
+ | `oracle` | When the deliverable is… | Evidence the evaluator cites |
59
+ |---|---|---|
60
+ | `ui` *(default)* | a running web app | accessibility-tree node, state before/after, console |
61
+ | `process` | a CLI / script | spawned exit code + stdout/stderr + crash check |
62
+ | `test` | a library / module | the project's own test suite (exit + failing-test names) |
63
+ | `snapshot` | a generator / pure refactor | unified diff vs a golden file (empty = PASS) |
64
+ | `http` | a service / API | response status + body assertion |
65
+
66
+ **Rules:**
67
+ - **Default is `ui`.** A row (or AC) that omits `Oracle` is treated as `ui` — pre-v2.9 specs and
68
+ existing web pitches are unchanged.
69
+ - **Pick the oracle from the deliverable type, not the test idea.** A `todo` CLI's `TS-*` rows are
70
+ `process`; a shared library's are `test`; an HTTP endpoint's are `http`. When a UC's deliverable
71
+ is non-UI, set the oracle on every derived row so the evaluator does not fall back to driving a
72
+ browser that does not exist.
73
+ - **The expectation must be observable by that oracle.** A `process` row's Expect is phrased in
74
+ exit code + stdout (e.g. *"exit ≠ 0, message names the file, no stack trace"*); a `test` row's is
75
+ *"suite green, the named case passes"*. Never phrase a row so its only check is reading source.
76
+ - This is the half of the goal that makes **"build anything"** real: the same Test Surface
77
+ discipline now derives evaluable rows for a CLI or a library, not only a browser app.
78
+
79
+ ---
80
+
81
+ ## Generation points
82
+
83
+ 1. **Phase 4 (fresh run, v2.9+):** derived per UC immediately after Error Cases are
84
+ written, included in the GATE 4 review.
85
+ 2. **retrofit-surface operation (retrofit, incremental reducer):** for pre-v2.9 specs.
86
+ Frozen-zone discipline identical to the reconcile operation (the order's substrate enforces it):
87
+ - READ-ONLY: domain-model, UC Steps/Input/Output/Invariants, ux-behavior, contracts/, tasks/
88
+ - WRITE: append `## Test Surface` to each UC (after Error Cases; skip UCs that already
89
+ have one unless `--force-surface`), log touched UCs in `run-state.human_edited_files`,
90
+ set `run-state.test_surface: true`. Regenerate nothing else.
91
+ - Source material missing (no Invariants, no contracts on lite) → derive from what
92
+ exists; a UC whose four sources are all empty gets a one-line section:
93
+ `_No derivable surface — sources empty. Exploratory coverage only (see qa-edge-hunter)._`
94
+ 3. **reconcile operation:** when a new `[INV-NN]` is appended to a UC, also
95
+ append its `TS-INV-NN` row (same append-only discipline, same `human_edited_files` log).
96
+
97
+ ## Audit hooks (spec-lint)
98
+ - L2: every UC (v2.9 spec or `test_surface: true`) has `## Test Surface` with ≥1 row or the
99
+ explicit empty-sources line.
100
+ - L3: every `[INV-NN]` has a matching `TS-INV-NN` row; every Error Case code has a `TS-ERR-*`
101
+ row; every TS row cites D1–D4 in Source; the `Oracle` is one of the registry values (or omitted
102
+ ⇒ `ui`), and is non-`ui` when the UC's deliverable has no browser (CLI/library/service).
@@ -0,0 +1,269 @@
1
+ # UX Behavior Patterns
2
+
3
+ Reference for Phase 3 of the BA Pitch Analyzer. Read before writing `ux-behavior.md`.
4
+
5
+ ---
6
+
7
+ ## Deriving Screens From Pitch
8
+
9
+ From the pitch breadboarding and fat marker sketches, identify:
10
+
11
+ 1. **Entry points** — how does the user reach this feature?
12
+ 2. **Core interaction** — the primary action the user takes
13
+ 3. **Decision points** — where does the flow branch?
14
+ 4. **Exit points** — success path, failure path, abandon path
15
+
16
+ Each decision point is typically a screen boundary.
17
+
18
+ ---
19
+
20
+ ## State Machine per Screen
21
+
22
+ Every screen must have a state table. States map to domain aggregate statuses from
23
+ `domain-model.md`. Use this as a checklist:
24
+
25
+ | State | Always Present? | Notes |
26
+ |-------|----------------|-------|
27
+ | `idle` / `default` | ✅ | Initial mount state |
28
+ | `loading` / `submitting` | ✅ | Any async operation |
29
+ | `error` | ✅ | Inline field errors vs. page-level errors |
30
+ | `empty` | ⚠️ | When list/data can be empty |
31
+ | `success` / `done` | ✅ for forms | Show confirmation, not just redirect |
32
+ | `disabled` | ⚠️ | When action is conditionally unavailable |
33
+
34
+ ### State Table Format
35
+
36
+ ```markdown
37
+ | State | Trigger | UI Behavior | CTA State |
38
+ |-------|---------|-------------|-----------|
39
+ | `idle` | screen mount | form editable | enabled |
40
+ | `validating` | user taps submit | form locked, spinner on CTA | loading |
41
+ | `field-error` | validation fails | inline error messages shown | enabled |
42
+ | `submitting` | all validation passes | full-screen loader | hidden |
43
+ | `success` | API response 200 | redirect or success toast | — |
44
+ | `error` | API response 4xx/5xx | error banner, form re-enabled | enabled |
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Behavior Rules Format
50
+
51
+ Document explicit rules that aren't obvious from the state machine:
52
+
53
+ ```markdown
54
+ ### Behavior Rules
55
+ - [RULE-01] CTA is disabled until all required fields pass validation
56
+ - [RULE-02] Phone number field auto-formats to `0XX XXX XXXX` on blur
57
+ - [RULE-03] If `totalAmount < 10,000 VND`, disable VNPay option with tooltip
58
+ - [RULE-04] Auto-save draft every 30 seconds when form is dirty
59
+ - [RULE-05] Back navigation shows confirmation dialog if form is dirty
60
+ ```
61
+
62
+ Number rules so tasks can reference them: `implements [[ux-behavior#RULE-03]]`
63
+
64
+ ---
65
+
66
+ ## Error State Catalog
67
+
68
+ Every screen must have an explicit error catalog. Categorize:
69
+
70
+ | Category | Examples | UX Treatment |
71
+ |---|---|---|
72
+ | **Validation** | Required field, invalid format | Inline, under field, immediate |
73
+ | **Business rule** | Insufficient stock, limit exceeded | Inline banner, blocking |
74
+ | **Network** | Timeout, offline | Retry button, non-blocking toast |
75
+ | **Auth** | Session expired, unauthorized | Redirect to login |
76
+ | **Server** | 500, unexpected | Generic error page with support link |
77
+
78
+ ```markdown
79
+ ### Error Catalog: [ScreenName]
80
+
81
+ | Error Code | Condition | User Message | Action |
82
+ |---|---|---|---|
83
+ | `NETWORK_TIMEOUT` | No response in 30s | "Connection is slow, please try again" | [Retry] button |
84
+ | `ITEM_UNAVAILABLE` | Stock = 0 at checkout | "Product {name} is out of stock" | [View similar products] |
85
+ | `SESSION_EXPIRED` | Token expired | "Your session has expired" | [Log in again] → redirect |
86
+ | `PAYMENT_DECLINED` | VNPay returns fail | "Payment failed. Check your account." | [Try another method] |
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Screen Flow Diagrams
92
+
93
+ Use ASCII for flows — no Mermaid (Claude Code terminals can't render it):
94
+
95
+ ```
96
+ [CartScreen]
97
+
98
+ ├─ empty cart ──► [EmptyCartScreen]
99
+
100
+ └─ confirm ──► [CheckoutScreen]
101
+
102
+ ┌────────┴────────┐
103
+ │ │
104
+ address ok address error
105
+ │ │
106
+ [PaymentScreen] [AddressFormScreen]
107
+
108
+ ┌───────┼───────┐
109
+ │ │ │
110
+ vnpay momo cod
111
+ │ │ │
112
+ └───────┴───┐ │
113
+ │ └──► [ConfirmationScreen] (cod)
114
+ [VNPayRedirect]
115
+
116
+ ┌───────┴───────┐
117
+ │ │
118
+ success failed
119
+ │ │
120
+ [ConfirmationScreen] [PaymentFailedScreen]
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Mobile vs Web Considerations
126
+
127
+ Document when behavior differs by platform:
128
+
129
+ ```markdown
130
+ ### Platform Differences: [ScreenName]
131
+
132
+ | Behavior | Mobile | Web |
133
+ |---|---|---|
134
+ | Payment redirect | Deep link back to app | Popup or same tab |
135
+ | Address input | Native maps picker | Text autocomplete (Goong) |
136
+ | Photo upload | Camera or gallery sheet | File picker |
137
+ | Session expired | Toast + slide to login | Modal |
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Vietnam-Specific UX Patterns
143
+
144
+ Flag these when relevant to the pitch:
145
+
146
+ - **VNPay redirect:** User leaves app → payment gateway → returns via deep link or webhook
147
+ - **Zalo ZNS:** Transactional messages (not push) — user must be Zalo friend of OA
148
+ - **MoMo deep link:** `momo://...` schema, must handle fallback to web
149
+ - **Phone as primary identifier:** Format `0XX XXX XXXX`, normalize to `+84` for storage
150
+ - **Vietnamese address:** Province → District → Ward → Street — hierarchical dropdown
151
+
152
+ ---
153
+
154
+ ## LITE Lens — ux-behavior.md is the Primary Spec
155
+
156
+ When `lens: lite`, `ux-behavior.md` is the **most authoritative document** in the spec tree.
157
+ It must be more exhaustive than in STANDARD mode. Apply these additional requirements:
158
+
159
+ ### Required additions for LITE
160
+
161
+ **1. Navigation Stack**
162
+
163
+ Document the full navigation hierarchy — not just screen flow:
164
+
165
+ ```
166
+ Stack Navigator: AppStack
167
+ ├── Tab: HomeTab → HomeScreen
168
+ ├── Tab: OrderTab → OrderListScreen
169
+ │ └── push → OrderDetailScreen
170
+ │ └── push → CheckoutScreen ← this feature
171
+ │ └── modal → PaymentWebViewScreen
172
+ └── Tab: ProfileTab → ProfileScreen
173
+ ```
174
+
175
+ **2. Offline Behavior Rules**
176
+
177
+ For every screen that reads or writes data, document offline behavior explicitly:
178
+
179
+ | Action | Online | Offline | Sync Strategy |
180
+ |--------|--------|---------|--------------|
181
+ | Load order list | fetch API | show cached data | stale-while-revalidate |
182
+ | Submit order | POST /orders | queue locally | sync on reconnect |
183
+ | Upload photo | PUT /photos | queue locally | retry × 3 on reconnect |
184
+
185
+ **3. Gesture and Interaction Specs**
186
+
187
+ Document non-obvious interactions explicitly — these are not derivable from API contracts:
188
+
189
+ ```
190
+ - Pull-to-refresh: triggers full reload, resets pagination
191
+ - Long press on order card: show context menu (copy ID, share)
192
+ - Swipe left on list item: reveal delete action (iOS) / show checkbox (Android)
193
+ - Back gesture during payment: confirm dialog — "Leave payment? Your cart is saved."
194
+ ```
195
+
196
+ **4. Platform Differences (mandatory, not optional)**
197
+
198
+ Every screen must have a Platform Differences sub-section when behavior diverges:
199
+
200
+ ```markdown
201
+ ### Platform Differences: CheckoutScreen
202
+
203
+ | Behavior | iOS | Android |
204
+ |----------|-----|---------|
205
+ | Address picker | Apple Maps sheet | Google Maps bottom sheet |
206
+ | Payment deeplink return | Universal Link | Intent filter |
207
+ | Keyboard avoidance | KeyboardAvoidingView behavior="padding" | behavior="height" |
208
+ | haptic feedback | UIImpactFeedbackGenerator | Vibrator.vibrate(50) |
209
+ ```
210
+
211
+ **5. API Stub Contracts (LITE only)**
212
+
213
+ In LITE mode, tasks use typed fetch wrappers without formal contracts.
214
+ Document the assumed API shape inline in ux-behavior.md as a stub:
215
+
216
+ ```typescript
217
+ // Assumed API stub — will be replaced by contracts/ in STANDARD upgrade
218
+ interface CheckoutAPI {
219
+ submitOrder(payload: SubmitOrderPayload): Promise<{ orderId: string }>
220
+ getPaymentUrl(orderId: string): Promise<{ url: string }>
221
+ }
222
+ // Source: [[usecases/UC-SubmitOrder#Steps]]
223
+ ```
224
+
225
+ This stub is the handoff artifact to the API team if/when STANDARD upgrade happens.
226
+
227
+ ---
228
+
229
+ ## Visual Design Contracts & Figma Integration (Co-authored by Product Designer)
230
+
231
+ To prevent discrepancies between Figma designs and final UI implementation, the Product Designer and BA co-author the visual section of each screen specification.
232
+
233
+ ### 1. Figma Reference Mapping
234
+ For every screen, specify the exact Figma Frame link in the header:
235
+ ```markdown
236
+ Figma Frame: https://www.figma.com/file/XYZ/AppName?node-id=123-456
237
+ ```
238
+
239
+ ### 2. Visual & Layout Spec Table
240
+ Define the layout model, alignment, spacing, and responsive behavior:
241
+ ```markdown
242
+ | Layout Property | Mobile View | Desktop View | Styling Implementation Details |
243
+ |---|---|---|---|
244
+ | **Structure** | Vertical block (`flex-col`) | Horizontal side-by-side (`flex-row`) | Flexbox with gap size 16px (`gap-4`) |
245
+ | **Grid / Columns** | 1 Column | 2 Columns (sidebar/main) | Sidebar: 300px width fixed, Main: flexible |
246
+ | **Spacing** | Padding 16px (`p-4`) | Padding 24px (`p-6`) | Keep alignment strictly centered |
247
+ | **Typography** | Body: 14px (`text-sm`) | Body: 16px (`text-base`) | Use Outfit font family |
248
+ ```
249
+
250
+ ### 3. Design Tokens Checklist
251
+ Every component must map to the project's standard CSS variables or Tailwind utility classes. Do not use ad-hoc hex codes or absolute layout offsets:
252
+ ```markdown
253
+ | Element | Visual Style / Token | Class/Property | Figma Source |
254
+ |---|---|---|---|
255
+ | Card Container | Rounded border, light shadow | `bg-card rounded-2xl shadow-sm` | Card Component |
256
+ | CTA Button | Brand primary background, bold text | `bg-primary text-white font-bold h-12` | Primary Button |
257
+ | Error Message | Brand red text, small font | `text-danger text-xs font-semibold` | Input/Alert Error |
258
+ | Border | Divider line | `border-gray-200` | Border line |
259
+ ```
260
+
261
+ ### 4. Interactive & State Design
262
+ Specify the visual styling changes for all active, hover, focused, disabled, and loading states:
263
+ ```markdown
264
+ - **Button Hover**: Darkens background by 10% (`hover:bg-primary-dark`)
265
+ - **Input Focus**: Border changes to brand color with subtle outline ring (`focus:border-primary focus:ring-1 focus:ring-primary`)
266
+ - **Disabled State**: Button opacity is reduced to 50% (`disabled:opacity-50 disabled:cursor-not-allowed`)
267
+ - **Loading Overlay**: Full-screen semi-transparent backdrop (`bg-black/40`) with center spinner
268
+ ```
269
+
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ // Board derivation (pure-skill architecture v1.0, plan §8.2).
3
+ //
4
+ // The mechanical half of the old ba-pitch-analyzer Phase 7b + v3.3 link-field rules — pure
5
+ // graph math a model should never re-derive (and could get wrong: KB-BA-001's 10 asymmetric
6
+ // edges came from hand-authored `unlocks`):
7
+ //
8
+ // • `unlocks` = the depends_on inverse, recomputed over the WHOLE board (--write persists
9
+ // it into task frontmatter — run on every ingest; asymmetric edges become impossible)
10
+ // • Σ estimated_hours, package distribution, critical path (longest depends_on chain)
11
+ // • Appetite Guard arithmetic (--appetite-hours N → overflow flag; the HAMMER *pause* on
12
+ // overflow is an orchestrator gate, never resolved here)
13
+ // • board-vs-T0 drift check (a FINISHED scope whose tasks still read `ready`) when scope
14
+ // contracts name their tasks — flag, never fix
15
+ //
16
+ // Zero dependencies. Usage:
17
+ // node skills/ba-pitch-analyzer/scripts/board-derive.mjs --slug <slug> [--cwd <dir>] [--write]
18
+ // [--appetite-hours N]
19
+ // Prints a JSON report; exit 0 (drift/overflow are flags for the caller's gate, not errors).
20
+
21
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
22
+ import { resolve, join } from "node:path";
23
+ import { isMain } from "../../tech-lead/scripts/lib/is-main.mjs";
24
+ import { runArgs } from "../../tech-lead/scripts/lib/argv.mjs";
25
+ import { tasksDir, scopesDir, hillDir } from "../../tech-lead/scripts/lib/paths.mjs";
26
+ import { readAllContracts, splitFrontmatter, SCOPE_CONTRACT } from "../../tech-lead/scripts/lib/contract-md.mjs";
27
+
28
+ /**
29
+ * Read a list field from a frontmatter string, inline `[a, b]` or YAML block sequence alike.
30
+ *
31
+ * HD-004 — the SECOND instance of HD-003, in a second parser. This repo had two hand-rolled
32
+ * frontmatter readers: `contract-md.mjs` for the committed contracts, and this one for the board.
33
+ * HD-003 was fixed in the first, and this one still silently dropped a block sequence — so a board
34
+ * written as
35
+ * use_case_refs:
36
+ * - UC-01
37
+ * touched_files:
38
+ * - src/review/format.js
39
+ * lost EVERY list field. Measured on three paid `ba-pitch-analyzer` runs: `touched_files` and
40
+ * `use_case_refs` came back empty for all twenty tasks, `depends_on`/`unlocks` came back empty in
41
+ * both directions so edge-symmetry passed VACUOUSLY, and the only reason it was not entirely silent
42
+ * is that UC-ANCHOR went red. Downstream, `scope-architect` receives a board with no slicing input
43
+ * at all.
44
+ *
45
+ * The fix is to stop having two parsers rather than to patch this one: `splitFrontmatter` already
46
+ * reads both forms, so this delegates to it. A second implementation of a format is a second
47
+ * format, and these two had already drifted.
48
+ *
49
+ * @param {string} fm - The frontmatter text (without the `---` fences).
50
+ * @param {string} key - The list key to read.
51
+ * @returns {string[]} The trimmed, unquoted, non-empty members; [] when the key is absent.
52
+ */
53
+ const listField = (fm, key) => {
54
+ const v = splitFrontmatter(`---\n${fm}\n---\n`).meta[key];
55
+ if (Array.isArray(v)) return v.map((x) => String(x).trim()).filter(Boolean);
56
+ if (v === null || v === undefined || v === "") return [];
57
+ return [String(v).trim()].filter(Boolean);
58
+ };
59
+
60
+ /**
61
+ * Parse every TASK-*.md in a board directory into structured task records.
62
+ * @param {string} tasksDir - Absolute path to the LOCAL tasks directory.
63
+ * @returns {Array<{file:string, id:string, type:string, status:string, hours:number, pkg:string,
64
+ * depends_on:string[], unlocks:string[], use_case_refs:string[], body:string}>} One record per
65
+ * task file; [] when the directory does not exist.
66
+ */
67
+ export function parseBoard(tasksDir) {
68
+ if (!existsSync(tasksDir)) return [];
69
+ return readdirSync(tasksDir)
70
+ .filter((f) => /^TASK-[\w.-]+\.md$/i.test(f))
71
+ .map((f) => {
72
+ const body = readFileSync(join(tasksDir, f), "utf8");
73
+ const fm = (body.match(/^---\r?\n([\s\S]*?)\r?\n---/) || [, ""])[1];
74
+ return {
75
+ file: join(tasksDir, f),
76
+ id: (fm.match(/^id:\s*(TASK-[\w.-]+)/im) || [])[1] || f.replace(/\.md$/, ""),
77
+ type: (fm.match(/^type:\s*(\S+)/im) || [, ""])[1],
78
+ status: (fm.match(/^status:\s*(\S+)/im) || [, "unknown"])[1],
79
+ hours: Number((fm.match(/^estimated_hours:\s*([\d.]+)/im) || [])[1]) || 0,
80
+ pkg: (fm.match(/^package:\s*(.+)$/im) || [, ""])[1].trim(),
81
+ depends_on: listField(fm, "depends_on"),
82
+ unlocks: listField(fm, "unlocks"),
83
+ use_case_refs: listField(fm, "use_case_refs"),
84
+ body,
85
+ };
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Compute `unlocks` as the inverse of `depends_on` over the whole board (never hand-authored).
91
+ * @param {Array<{id:string, depends_on:string[]}>} tasks - The parsed board.
92
+ * @returns {Object<string,string[]>} Map of task id → sorted ids it unlocks.
93
+ */
94
+ export function deriveUnlocks(tasks) {
95
+ const unlocks = Object.fromEntries(tasks.map((t) => [t.id, []]));
96
+ for (const t of tasks) for (const dep of t.depends_on) if (unlocks[dep]) unlocks[dep].push(t.id);
97
+ for (const id of Object.keys(unlocks)) unlocks[id].sort();
98
+ return unlocks;
99
+ }
100
+
101
+ /**
102
+ * Find the critical path: the depends_on chain with the greatest summed hours.
103
+ * @param {Array<{id:string, depends_on:string[], hours:number}>} tasks - The parsed board.
104
+ * @returns {{hours:number, chain:string[]}} The longest chain's total hours and its task ids in
105
+ * dependency order (cycles are guarded and contribute nothing).
106
+ */
107
+ export function criticalPath(tasks) {
108
+ const byId = Object.fromEntries(tasks.map((t) => [t.id, t]));
109
+ const memo = {};
110
+ /**
111
+ * Longest depends_on chain rooted at a task, by summed hours (memoized, cycle-guarded).
112
+ * @param {string} id - Task id to start from.
113
+ * @param {Set<string>} [seen=new Set()] - Ids on the current DFS path (cycle guard); callers omit it.
114
+ * @returns {{hours:number, chain:string[]}} The heaviest chain's total hours and its task ids in
115
+ * dependency order; {hours:0, chain:[]} for an unknown id or a detected cycle.
116
+ */
117
+ const longest = (id, seen = new Set()) => {
118
+ if (memo[id]) return memo[id];
119
+ if (seen.has(id)) return { hours: 0, chain: [] }; // cycle guard — lint reports it separately
120
+ seen.add(id);
121
+ const t = byId[id];
122
+ if (!t) return { hours: 0, chain: [] };
123
+ let best = { hours: 0, chain: [] };
124
+ for (const dep of t.depends_on) {
125
+ const c = longest(dep, seen);
126
+ if (c.hours > best.hours) best = c;
127
+ }
128
+ return (memo[id] = { hours: best.hours + t.hours, chain: [...best.chain, id] });
129
+ };
130
+ let best = { hours: 0, chain: [] };
131
+ for (const t of tasks) {
132
+ const c = longest(t.id);
133
+ if (c.hours > best.hours) best = c;
134
+ }
135
+ return best;
136
+ }
137
+
138
+ /**
139
+ * Flag board-vs-T0 drift: FINISHED scopes whose named tasks are not yet done (flag only, never fix).
140
+ * @param {Array<{id:string, status:string}>} tasks - The parsed board.
141
+ * @param {Array<{scope_id:string, tasks?:string[], finished?:boolean}>} scopes - Scope facts.
142
+ * @returns {Array<{scope_id:string, task_id:string, status:string}>} One entry per drifting task; [] when none.
143
+ */
144
+ export function driftCheck(tasks, scopes) {
145
+ const byId = Object.fromEntries(tasks.map((t) => [t.id, t]));
146
+ const drift = [];
147
+ for (const s of scopes) {
148
+ if (!Array.isArray(s.tasks) || !s.finished) continue;
149
+ for (const id of s.tasks) {
150
+ const t = byId[id];
151
+ if (t && t.status !== "done") drift.push({ scope_id: s.scope_id, task_id: id, status: t.status });
152
+ }
153
+ }
154
+ return drift;
155
+ }
156
+
157
+ /**
158
+ * Derive the full board report (unlocks, hours, packages, critical path, appetite overflow, drift).
159
+ * @param {{cwd:string, slug:string, appetiteHours?:(number|null)}} opts - Working root, feature
160
+ * slug, and optional appetite budget (in hours) that drives the overflow flag.
161
+ * @returns {object} The report: task_count, by_status, packages, total/keep hours, appetite
162
+ * overflow (or null), critical_path, unlocks + unlocks_stale, drift[], and `_tasks` (the parsed
163
+ * board, deleted before CLI output).
164
+ */
165
+ export function derive({ cwd, slug, appetiteHours = null }) {
166
+ const tasksDir = tasksDir(cwd, slug);
167
+ const tasks = parseBoard(tasksDir);
168
+ const unlocks = deriveUnlocks(tasks);
169
+ const keepHours = tasks.filter((t) => t.status !== "cut").reduce((a, t) => a + t.hours, 0);
170
+ const packages = {};
171
+ for (const t of tasks) packages[t.pkg || "(none)"] = (packages[t.pkg || "(none)"] || 0) + 1;
172
+
173
+ // Scope facts for the drift check: contract `tasks` list + committed hill shard phase.
174
+ const hillRoot = hillDir(cwd, slug);
175
+ const scopes = readAllContracts(scopesDir(cwd, slug), SCOPE_CONTRACT).map(({ contract }) => {
176
+ const shard = join(hillRoot, `${contract.scope_id}.yml`);
177
+ const finished = existsSync(shard) && /phase:\s*FINISHED/.test(readFileSync(shard, "utf8"));
178
+ return { ...contract, finished };
179
+ });
180
+
181
+ return {
182
+ slug,
183
+ task_count: tasks.length,
184
+ by_status: tasks.reduce((m, t) => ((m[t.status] = (m[t.status] || 0) + 1), m), {}),
185
+ packages,
186
+ total_hours: tasks.reduce((a, t) => a + t.hours, 0),
187
+ keep_hours: keepHours,
188
+ appetite: appetiteHours === null ? null : {
189
+ appetite_hours: appetiteHours,
190
+ overflow: keepHours > appetiteHours,
191
+ overflow_hours: Math.max(0, keepHours - appetiteHours),
192
+ },
193
+ critical_path: criticalPath(tasks),
194
+ unlocks,
195
+ unlocks_stale: tasks.filter((t) => JSON.stringify([...t.unlocks].sort()) !== JSON.stringify(unlocks[t.id])).map((t) => t.id),
196
+ drift: driftCheck(tasks, scopes),
197
+ _tasks: tasks,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Persist derived `unlocks` into task frontmatter — the ONE write this script makes.
203
+ * @param {{_tasks:Array<object>, unlocks:Object<string,string[]>}} report - A {@link derive} report.
204
+ * @returns {string[]} The ids of task files actually rewritten (unchanged files are skipped).
205
+ * Side effect: writes those task files.
206
+ */
207
+ export function writeUnlocks(report) {
208
+ const written = [];
209
+ for (const t of report._tasks) {
210
+ const want = `[${report.unlocks[t.id].join(", ")}]`;
211
+ const fmMatch = t.body.match(/^---\r?\n([\s\S]*?)\r?\n---/);
212
+ if (!fmMatch) continue;
213
+ const fm = fmMatch[1];
214
+ const next = /^unlocks:.*$/m.test(fm) ? fm.replace(/^unlocks:.*$/m, `unlocks: ${want}`) : `${fm}\nunlocks: ${want}`;
215
+ if (next !== fm) {
216
+ writeFileSync(t.file, t.body.replace(fm, next));
217
+ written.push(t.id);
218
+ }
219
+ }
220
+ return written;
221
+ }
222
+
223
+ /** The typed argv contract (see `skills/tech-lead/scripts/lib/argv.mjs`). */
224
+ export const ARGV_SPEC = {
225
+ usage: "board-derive.mjs --slug <slug> [--cwd <dir>] [--write] [--appetite-hours N]",
226
+ _: { arity: 0, max: 0, name: "(no positional operands)" },
227
+ slug: { type: "str", required: true },
228
+ cwd: { type: "path" },
229
+ write: { type: "flag" },
230
+ "appetite-hours": { type: "num", min: 0 },
231
+ };
232
+
233
+ const isMainModule = isMain(import.meta.url);
234
+ if (isMainModule) {
235
+ const args = runArgs(ARGV_SPEC);
236
+ const cwd = resolve(args.cwd || process.cwd());
237
+ const report = derive({ cwd, slug: args.slug, appetiteHours: args.appetiteHours ?? null });
238
+ if (args.write) report.unlocks_written = writeUnlocks(report);
239
+ delete report._tasks;
240
+ console.log(JSON.stringify(report, null, 2));
241
+ }