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.
- package/.claude/settings.local.example.json +20 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.env.shapeup.example +14 -0
- package/AGENTS.md +133 -0
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/SECURITY.md +72 -0
- package/bin/init.mjs +329 -0
- package/commands/build.md +14 -0
- package/commands/eval.md +15 -0
- package/commands/hammer.md +12 -0
- package/commands/orient.md +11 -0
- package/commands/qa.md +12 -0
- package/commands/retro.md +13 -0
- package/commands/scopes.md +14 -0
- package/commands/shape.md +12 -0
- package/commands/ship.md +53 -0
- package/commands/wire.md +11 -0
- package/hooks/anti-rationalization.mjs +244 -0
- package/hooks/compact-snapshot.mjs +47 -0
- package/hooks/gate-deadline.mjs +151 -0
- package/hooks/gate-intake.mjs +110 -0
- package/hooks/gate-l2.mjs +161 -0
- package/hooks/gate-zerowork.mjs +264 -0
- package/hooks/hooks.json +118 -0
- package/hooks/lib/decision.mjs +183 -0
- package/hooks/safety-spine.mjs +296 -0
- package/hooks/sandbox-guard.mjs +172 -0
- package/hooks/session-rehydrate.mjs +109 -0
- package/hooks/slop-cleaner.mjs +176 -0
- package/oracles/_shared.mjs +46 -0
- package/oracles/http-oracle.mjs +155 -0
- package/oracles/index.mjs +36 -0
- package/oracles/process-oracle.mjs +146 -0
- package/oracles/snapshot-oracle.mjs +119 -0
- package/oracles/test-oracle.mjs +138 -0
- package/package.json +49 -0
- package/skills/advisor-protocol/SKILL.md +171 -0
- package/skills/ba-pitch-analyzer/SKILL.md +175 -0
- package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +84 -0
- package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +65 -0
- package/skills/ba-pitch-analyzer/assets/templates/assess-report.tmpl.md +127 -0
- package/skills/ba-pitch-analyzer/assets/templates/contracts/be-service.contract.tmpl.md +62 -0
- package/skills/ba-pitch-analyzer/assets/templates/contracts/offline-storage.contract.tmpl.md +92 -0
- package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +66 -0
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/context-map.tmpl.md +64 -0
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/event-choreography.tmpl.md +77 -0
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +104 -0
- package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +79 -0
- package/skills/ba-pitch-analyzer/assets/templates/domain-model.tmpl.md +79 -0
- package/skills/ba-pitch-analyzer/assets/templates/feedback.tmpl.md +87 -0
- package/skills/ba-pitch-analyzer/assets/templates/integration.tmpl.md +64 -0
- package/skills/ba-pitch-analyzer/assets/templates/run-state.tmpl.md +77 -0
- package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +105 -0
- package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +215 -0
- package/skills/ba-pitch-analyzer/assets/templates/task-board.tmpl.md +35 -0
- package/skills/ba-pitch-analyzer/assets/templates/task-spike.tmpl.md +86 -0
- package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +84 -0
- package/skills/ba-pitch-analyzer/assets/templates/usecase.tmpl.md +105 -0
- package/skills/ba-pitch-analyzer/assets/templates/ux-behavior.tmpl.md +65 -0
- package/skills/ba-pitch-analyzer/references/contract-patterns.md +152 -0
- package/skills/ba-pitch-analyzer/references/ddd-patterns.md +173 -0
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +275 -0
- package/skills/ba-pitch-analyzer/references/integration-analysis.md +152 -0
- package/skills/ba-pitch-analyzer/references/task-generation.md +623 -0
- package/skills/ba-pitch-analyzer/references/test-surface.md +102 -0
- package/skills/ba-pitch-analyzer/references/ux-behavior-patterns.md +269 -0
- package/skills/ba-pitch-analyzer/scripts/board-derive.mjs +241 -0
- package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +239 -0
- package/skills/coach/SKILL.md +202 -0
- package/skills/orient/SKILL.md +262 -0
- package/skills/qa-edge-hunter/SKILL.md +369 -0
- package/skills/scope-architect/SKILL.md +111 -0
- package/skills/scope-hammer/SKILL.md +186 -0
- package/skills/shapeup/SKILL.md +403 -0
- package/skills/shapeup/resources/breadboard-reflection.md +88 -0
- package/skills/shapeup/resources/breadboarding.md +334 -0
- package/skills/shapeup/resources/context-compaction.md +240 -0
- package/skills/shapeup/resources/framing-doc.md +92 -0
- package/skills/shapeup/resources/kickoff-doc.md +95 -0
- package/skills/shapeup/resources/shaping.md +194 -0
- package/skills/shapeup/resources/spike.md +86 -0
- package/skills/solution-architect/SKILL.md +129 -0
- package/skills/spec-evaluator/README.md +93 -0
- package/skills/spec-evaluator/SKILL.md +213 -0
- package/skills/spec-evaluator/references/anti-leniency.md +50 -0
- package/skills/spec-evaluator/references/dimension-contract.md +126 -0
- package/skills/spec-evaluator/references/dimensions/_registry.md +40 -0
- package/skills/spec-evaluator/references/dimensions/completeness.md +102 -0
- package/skills/spec-evaluator/references/dimensions/integration.md +129 -0
- package/skills/spec-evaluator/references/dimensions/performance.md +48 -0
- package/skills/spec-evaluator/references/dimensions/security.md +60 -0
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +134 -0
- package/skills/spec-evaluator/references/dimensions/tdd-surface.md +110 -0
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +100 -0
- package/skills/spec-evaluator/references/probing.md +177 -0
- package/skills/spec-evaluator/references/report-schema.md +101 -0
- package/skills/spec-evaluator/references/verdict-ledger.md +92 -0
- package/skills/spec-evaluator/scripts/verdict-ledger.mjs +166 -0
- package/skills/task-executor/SKILL.md +194 -0
- package/skills/tech-lead/README.md +71 -0
- package/skills/tech-lead/SKILL.md +459 -0
- package/skills/tech-lead/references/delegation.md +254 -0
- package/skills/tech-lead/references/gates.md +379 -0
- package/skills/tech-lead/references/invocation.md +45 -0
- package/skills/tech-lead/references/ledger-schema.md +214 -0
- package/skills/tech-lead/references/round-protocol.md +184 -0
- package/skills/tech-lead/references/state-model.md +66 -0
- package/skills/tech-lead/references/tiny-lane.md +52 -0
- package/skills/tech-lead/schemas/domain.schema.json +2294 -0
- package/skills/tech-lead/schemas/gate-answers.schema.json +92 -0
- package/skills/tech-lead/schemas/work-order.schema.json +21 -0
- package/skills/tech-lead/schemas/work-result.schema.json +40 -0
- package/skills/tech-lead/scripts/aegis-digest.mjs +124 -0
- package/skills/tech-lead/scripts/budget-check.mjs +156 -0
- package/skills/tech-lead/scripts/compile-order.mjs +518 -0
- package/skills/tech-lead/scripts/fit-check.mjs +196 -0
- package/skills/tech-lead/scripts/gate-answers.mjs +338 -0
- package/skills/tech-lead/scripts/ingest-result.mjs +270 -0
- package/skills/tech-lead/scripts/init-run.mjs +326 -0
- package/skills/tech-lead/scripts/lib/argv.mjs +224 -0
- package/skills/tech-lead/scripts/lib/contract-md.mjs +481 -0
- package/skills/tech-lead/scripts/lib/is-main.mjs +82 -0
- package/skills/tech-lead/scripts/lib/paths.mjs +278 -0
- package/skills/tech-lead/scripts/lib/ratchet-tree.mjs +112 -0
- package/skills/tech-lead/scripts/run-snapshot.mjs +269 -0
- package/skills/tech-lead/scripts/ship-report.mjs +275 -0
- package/skills/tech-lead/scripts/stats.mjs +423 -0
- package/skills/tech-lead/scripts/t0-verify.mjs +470 -0
- package/skills/tech-lead/scripts/trace-lint.mjs +367 -0
- package/skills/tech-lead/scripts/validate-envelope.mjs +283 -0
- package/skills/translator/README.md +66 -0
- package/skills/translator/SKILL.md +258 -0
- 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
|
+
}
|