featureparity 0.0.13 → 0.0.15
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.
- checksums.yaml +4 -4
- data/lib/fp/cli.rb +4 -1
- data/lib/fp/client.rb +84 -0
- data/lib/fp/commands/attach.rb +190 -0
- data/lib/fp/commands/attachments.rb +122 -0
- data/lib/fp/commands/check.rb +30 -7
- data/lib/fp/commands/ci_report.rb +5 -1
- data/lib/fp/commands/conversation.rb +92 -0
- data/lib/fp/commands/help.rb +64 -9
- data/lib/fp/commands/init.rb +87 -21
- data/lib/fp/commands/propose.rb +23 -2
- data/lib/fp/commands/propose_change.rb +13 -5
- data/lib/fp/commands/report.rb +19 -3
- data/lib/fp/commands/show.rb +14 -0
- data/lib/fp/commands.rb +3 -0
- data/lib/fp/junit.rb +117 -28
- data/lib/fp/version.rb +1 -1
- data/skills/fp/SKILL.md +110 -25
- data/skills/fp/references/cli.md +62 -23
- data/skills/fp/references/markers.md +25 -0
- metadata +5 -2
data/lib/fp/commands/help.rb
CHANGED
|
@@ -23,6 +23,11 @@ module Fp
|
|
|
23
23
|
propose-change <slug> ... Propose a change to an existing requirement
|
|
24
24
|
propose-change <slug> --removal Propose deleting a requirement
|
|
25
25
|
history <slug> --project <slug> Show a requirement's change history + diffs
|
|
26
|
+
attach <slug> <file> Upload a file attachment to a requirement
|
|
27
|
+
attachments <slug> List attachments for a requirement
|
|
28
|
+
attachments <slug> <id> Get download URL for an attachment
|
|
29
|
+
attach rm <slug> <id> Delete an attachment
|
|
30
|
+
conversation <id> Read a Pari conversation for agent review
|
|
26
31
|
report <slug> --project <slug>... Report evidence for a requirement
|
|
27
32
|
report --junit <file> ... Upload evidence from a JUnit XML report (no CI)
|
|
28
33
|
ci-report --junit <file> ... Ingest CI test results (passing/failing) — CI only
|
|
@@ -151,16 +156,45 @@ module Fp
|
|
|
151
156
|
# Show a specific requirement
|
|
152
157
|
fp show print_container_qr --project stowzilla
|
|
153
158
|
|
|
154
|
-
# Propose a new requirement (creates as draft)
|
|
159
|
+
# Propose a new requirement (creates as draft).
|
|
160
|
+
# A requirement records the current shared understanding, not a decree handed from
|
|
161
|
+
# product to engineering. Refine it through conversation with customers, domain
|
|
162
|
+
# experts, and builders. Humans decide value and priority; agents surface gaps,
|
|
163
|
+
# inconsistencies, alternatives, and useful splits. Prefer outcomes over feature lists.
|
|
164
|
+
# Use available evidence: what people attempt, abandon, actually use, and what
|
|
165
|
+
# experiments reveal. The first requested solution is input, not unquestionable scope.
|
|
166
|
+
# First inventory the whole request and check active AND draft requirements. Give
|
|
167
|
+
# each independently testable/failable product promise a focused requirement; use
|
|
168
|
+
# a parent plus children for a broad capability with distinct behaviours or policy.
|
|
169
|
+
# Record concrete decisions (prices, quotas, dates, roles, fallbacks) in acceptance,
|
|
170
|
+
# and propose changes to adjacent requirements the work makes inaccurate.
|
|
171
|
+
# Before adding or changing a promise, ask whether it and related active/draft
|
|
172
|
+
# requirements can both be true for the same user, state, and conditions. Treat a
|
|
173
|
+
# failing bound test or required requirement edit as a contradiction signal: decide
|
|
174
|
+
# which promise reflects product intent and record that decision with propose-change
|
|
175
|
+
# (or --removal). Never weaken the test or add a requirement around the conflict;
|
|
176
|
+
# ask a human if intent is unclear.
|
|
155
177
|
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
178
|
+
# Write the requirement as a product decision someone can approve, not a design
|
|
179
|
+
# task. It should sound like a capable teammate, not AI boilerplate.
|
|
180
|
+
# title: one product promise in present tense, not the implementation project
|
|
181
|
+
# why: one or two natural sentences naming the user need/problem and outcome
|
|
182
|
+
# acceptance: trigger or situation + observable result + material boundaries
|
|
183
|
+
# Use examples and tests to ground and double-check the general rule, not to replace
|
|
184
|
+
# it or ongoing conversation. Leave out components, data models, algorithms, endpoints,
|
|
185
|
+
# and UI controls unless they are themselves part of the user-visible contract.
|
|
186
|
+
# Include a price, quota, timeout, count, date,
|
|
187
|
+
# role, or fallback only when the source made it a real product decision; if an
|
|
188
|
+
# important policy is missing, ask instead of inventing a plausible value.
|
|
189
|
+
#
|
|
190
|
+
# good title: "Dispatchers see current shipment status"
|
|
191
|
+
# good why: "Dispatchers need to spot delayed shipments before they miss a delivery window."
|
|
192
|
+
# good acceptance: "When status changes, the dashboard shows it; when unavailable, it says so instead of showing stale data as current."
|
|
193
|
+
# bad: "Implement Redis polling every 30 seconds to ensure a seamless experience."
|
|
194
|
+
# (invented policy, implementation detail, and filler; no product outcome)
|
|
195
|
+
# Before saving, read it as a manager deciding scope: is the value clear, is the
|
|
196
|
+
# result observable, are decisions separated from guesses, and does every sentence
|
|
197
|
+
# earn its place?
|
|
164
198
|
fp propose --project stowzilla \\
|
|
165
199
|
--slug print_container_qr \\
|
|
166
200
|
--name "Print QR code on container label" \\
|
|
@@ -182,6 +216,8 @@ module Fp
|
|
|
182
216
|
# — the card, the thread, the issue. The requirement drops back to DRAFT and a
|
|
183
217
|
# human reviews the old-vs-new diff before it counts again.
|
|
184
218
|
fp propose-change print_container_qr --project stowzilla \\
|
|
219
|
+
--name "Print a scannable container QR" \\
|
|
220
|
+
--slug print_scannable_container_qr \\
|
|
185
221
|
--why "Labels must survive a freezer" \\
|
|
186
222
|
--acceptance "QR scans after 24h at -20C" \\
|
|
187
223
|
--reason "Cold-chain rollout" \\
|
|
@@ -197,6 +233,25 @@ module Fp
|
|
|
197
233
|
fp history print_container_qr --project stowzilla
|
|
198
234
|
fp history print_container_qr --project stowzilla --pending
|
|
199
235
|
|
|
236
|
+
# Upload a file attachment to a requirement (screenshot evidence, docs, etc.)
|
|
237
|
+
# Supported formats: png, jpg, gif, webp, svg, pdf, txt, md (max 10 MB)
|
|
238
|
+
fp attach print_container_qr screenshot.png --project stowzilla
|
|
239
|
+
|
|
240
|
+
# List attachments for a requirement
|
|
241
|
+
fp attachments print_container_qr --project stowzilla
|
|
242
|
+
|
|
243
|
+
# Get a download URL for a specific attachment
|
|
244
|
+
fp attachments print_container_qr <attachment-id> --project stowzilla
|
|
245
|
+
|
|
246
|
+
# Delete an attachment
|
|
247
|
+
fp attach rm print_container_qr <attachment-id> --project stowzilla
|
|
248
|
+
|
|
249
|
+
# Read a Pari conversation so an agent can review AI suggestions.
|
|
250
|
+
# A conversation is just a record identified by its id — no --project
|
|
251
|
+
# needed, and it works for both project and account-level conversations.
|
|
252
|
+
# Use "Copy Reference" in Pari's header to get this command ready to paste.
|
|
253
|
+
fp conversation 03h0abc123xyz
|
|
254
|
+
|
|
200
255
|
# Report evidence for a requirement
|
|
201
256
|
fp report print_container_qr --project stowzilla \\
|
|
202
257
|
--surface customer_android \\
|
data/lib/fp/commands/init.rb
CHANGED
|
@@ -290,22 +290,60 @@ module Fp
|
|
|
290
290
|
If one is wrong or outdated, propose the change — never edit or ignore it.
|
|
291
291
|
|
|
292
292
|
**Start every task by checking the requirements — before you write code, not
|
|
293
|
-
after.** When you pick up a feature or bug, first
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
after.** When you pick up a feature or bug, first inventory the full request and
|
|
294
|
+
compare it with the active and draft requirements. Find what already covers the work,
|
|
295
|
+
what must change, and what is missing. Treating this as the opening move — not a chore
|
|
296
|
+
at the end — is what keeps the catalog trustworthy.
|
|
297
297
|
|
|
298
|
-
### 1. Find the
|
|
298
|
+
### 1. Find and model the requirements — do this first
|
|
299
299
|
|
|
300
300
|
```bash
|
|
301
301
|
fp list # active requirements (project comes from .featureparity.yml)
|
|
302
|
+
fp list --status draft # proposed work — check before creating duplicates
|
|
302
303
|
fp list --gaps # the ones with no evidence yet — good places to start
|
|
303
304
|
fp show <slug> # details, acceptance criteria, and existing receipts
|
|
304
305
|
fp history <slug> # why it says what it says, and whether a change is pending
|
|
305
306
|
```
|
|
306
307
|
|
|
307
|
-
|
|
308
|
-
|
|
308
|
+
**The requirement catalog is the human-readable product specification, not a box to
|
|
309
|
+
tick with one umbrella record.** It records the team's current shared understanding;
|
|
310
|
+
it is not a finished decree handed from product to engineering. Refine requirements
|
|
311
|
+
through ongoing conversation with customers, domain experts, and builders. The human
|
|
312
|
+
product owner decides value and priority; agents should surface gaps, inconsistencies,
|
|
313
|
+
alternatives, and useful splits rather than silently making product decisions.
|
|
314
|
+
|
|
315
|
+
Before coding, enumerate the independently meaningful promises in the request:
|
|
316
|
+
user-visible behaviours, policies, limits, plans, deadlines, state transitions,
|
|
317
|
+
permissions, failure modes, and affected surfaces. Then model them:
|
|
318
|
+
|
|
319
|
+
- Give each independently testable or independently failable product promise its own
|
|
320
|
+
focused requirement. If one acceptance sentence joins behaviours that could pass or
|
|
321
|
+
fail separately, split it.
|
|
322
|
+
- Use a parent for the broad capability and children for distinct behaviours, policies,
|
|
323
|
+
or surface-specific promises. Do not create requirements for implementation details
|
|
324
|
+
or refactors that make no product promise.
|
|
325
|
+
- Put concrete product decisions — prices, quotas, dates, roles, fallbacks — in the
|
|
326
|
+
requirement acceptance criteria. Never leave them only as assumptions in code or a PR.
|
|
327
|
+
- Check adjacent requirements and their history. New requirements do not excuse an old
|
|
328
|
+
requirement that the work makes inaccurate; propose a change to keep the whole catalog true.
|
|
329
|
+
- Before finishing, reconcile the implementation against the catalog: every shipped
|
|
330
|
+
behaviour maps to a current requirement, every changed promise is recorded, and every
|
|
331
|
+
focused requirement has appropriate marked evidence or an explicit gap. A broad marker
|
|
332
|
+
must never appear to prove several acceptance clauses that its test does not exercise.
|
|
333
|
+
|
|
334
|
+
**Prevent contradictions before they enter the catalog.** Before proposing a new or changed
|
|
335
|
+
promise, compare its intended acceptance criteria with related active and draft requirements
|
|
336
|
+
and read their history. Ask whether both promises can be true at the same time for the same
|
|
337
|
+
user, state, and conditions. If not, do not add another requirement.
|
|
338
|
+
|
|
339
|
+
A failing test bound to an existing requirement, or an implementation that only works by
|
|
340
|
+
changing one, is a contradiction signal — not permission to weaken the test or write a
|
|
341
|
+
second requirement around it. Identify which promise matches product intent, then use
|
|
342
|
+
`fp propose-change` (or `--removal`) with the source and reason to put that decision before
|
|
343
|
+
a human. If the intent is unclear, stop and ask rather than making the catalog say both things.
|
|
344
|
+
|
|
345
|
+
If no requirement covers what you're building, propose one or a parent/child tree. Agents
|
|
346
|
+
always create **drafts**; a human activates them:
|
|
309
347
|
|
|
310
348
|
```bash
|
|
311
349
|
fp propose --slug <slug> --name "..." --required <surface>,<surface> \\
|
|
@@ -313,18 +351,45 @@ module Fp
|
|
|
313
351
|
--source-type fizzy --source-url <card-or-issue-url>
|
|
314
352
|
```
|
|
315
353
|
|
|
316
|
-
**
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
354
|
+
**Write a requirement as a product decision someone can approve, not as a design
|
|
355
|
+
task.** It should read like a capable teammate wrote it: direct, specific, and free
|
|
356
|
+
of boilerplate such as “ensure a seamless experience.”
|
|
357
|
+
|
|
358
|
+
- **Title:** one clear product promise in present tense. Name the user-visible
|
|
359
|
+
capability, not the work (`“Dispatchers see current shipment status”`, not
|
|
360
|
+
`“Implement status polling”`).
|
|
361
|
+
- **Why:** one or two natural sentences naming who needs this, the problem or risk,
|
|
362
|
+
and the outcome worth protecting. Do not repeat the title, sell the feature, or
|
|
363
|
+
explain the mechanism.
|
|
364
|
+
- **Acceptance:** the observable behaviour that would let a reviewer decide whether
|
|
365
|
+
the promise is true. State the trigger or situation, the result, and material
|
|
366
|
+
boundaries such as permissions, failure states, and affected surfaces. Use examples
|
|
367
|
+
and marked tests to ground and double-check the rule. Do not mistake a few examples for the rule
|
|
368
|
+
or let tests stand for every case; neither replaces the general rule and continued
|
|
369
|
+
conversation.
|
|
370
|
+
- **Numbers and policy:** include a price, limit, timeout, date, role, or fallback only
|
|
371
|
+
when the source made it a real product decision. Never invent a plausible value to
|
|
372
|
+
fill a gap. If the decision matters and the source is silent, ask.
|
|
373
|
+
- **Implementation detail:** leave out components, data models, algorithms, endpoints,
|
|
374
|
+
and UI controls unless that interface or constraint is itself part of the promise.
|
|
375
|
+
|
|
376
|
+
Learn from the request, existing requirements and history, and available evidence about
|
|
377
|
+
what users actually do. Look for what they attempt, where they abandon a task, what they
|
|
378
|
+
use, and what experiments reveal. Treat the first proposed solution as input, not
|
|
379
|
+
unquestionable scope. Preserve important uncertainty as a question rather than
|
|
380
|
+
laundering a guess into acceptance criteria.
|
|
381
|
+
|
|
382
|
+
- ✅ **Title:** `Dispatchers see current shipment status`
|
|
383
|
+
**Why:** `Dispatchers need to spot delayed shipments before they miss a delivery window.`
|
|
384
|
+
**Acceptance:** `When a shipment's status changes, the dashboard shows the new status; if status is unavailable, it says so instead of presenting stale data as current.`
|
|
385
|
+
- ❌ **Title:** `Implement Redis-backed 30-second status polling`
|
|
386
|
+
**Why:** `This important feature ensures a seamless, robust experience.`
|
|
387
|
+
**Acceptance:** `Use a background worker and refresh every 30 seconds.`
|
|
388
|
+
This invents policy, prescribes a design, and never states the product outcome.
|
|
389
|
+
|
|
390
|
+
Before proposing, read the draft once as a manager deciding scope: is the value clear,
|
|
391
|
+
can the result be observed, are product decisions distinguished from guesses, and can
|
|
392
|
+
every sentence earn its place? If not, revise or ask before writing the record.
|
|
328
393
|
|
|
329
394
|
### 2. If the requirement is wrong, propose a change — don't silently edit it
|
|
330
395
|
|
|
@@ -347,8 +412,9 @@ module Fp
|
|
|
347
412
|
|
|
348
413
|
- Never edit a requirement's wording by any other route. `fp propose-change` is the
|
|
349
414
|
only one that records what it was and what prompted the change.
|
|
350
|
-
- You
|
|
351
|
-
|
|
415
|
+
- You may correct `--name` (the title) or `--slug` through a proposal. Existing evidence
|
|
416
|
+
follows a proposed slug, but every `fp:<slug>` marker still needs the same correction.
|
|
417
|
+
You still cannot remove a required surface.
|
|
352
418
|
- You cannot approve or reject — not even your own proposal.
|
|
353
419
|
- One open proposal per requirement (409 otherwise). Run `fp history <slug>` first.
|
|
354
420
|
- **A requirement awaiting review still counts toward the gate.** Proposing a change
|
data/lib/fp/commands/propose.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Fp
|
|
4
4
|
module Commands
|
|
5
|
-
# fp propose --project <slug> --slug X --name "..." --why "..." --required a,b,c --acceptance "..." --parent <slug> --category <cat>
|
|
5
|
+
# fp propose --project <slug> --slug X --name "..." --why "..." --required a,b,c --required-types unit,e2e --acceptance "..." --parent <slug> --category <cat>
|
|
6
6
|
# Create a new requirement as draft
|
|
7
7
|
class Propose < Base
|
|
8
8
|
KNOWN_FLAGS = {
|
|
@@ -11,6 +11,7 @@ module Fp
|
|
|
11
11
|
name: :string,
|
|
12
12
|
why: :string,
|
|
13
13
|
required: :string,
|
|
14
|
+
required_types: :string,
|
|
14
15
|
acceptance: :string,
|
|
15
16
|
parent: :string,
|
|
16
17
|
category: :string,
|
|
@@ -27,7 +28,7 @@ module Fp
|
|
|
27
28
|
opts, = parse_flags(args, KNOWN_FLAGS)
|
|
28
29
|
|
|
29
30
|
require_project!(opts)
|
|
30
|
-
require_flag(opts, :slug, '--slug is required (
|
|
31
|
+
require_flag(opts, :slug, '--slug is required (initial key for the requirement)')
|
|
31
32
|
require_flag(opts, :name, '--name is required (human-readable title)')
|
|
32
33
|
|
|
33
34
|
# Validate category client-side for better UX (fail fast with helpful message)
|
|
@@ -41,6 +42,9 @@ module Fp
|
|
|
41
42
|
exit 1
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
# Parse and validate required test types (comma-separated).
|
|
46
|
+
required_test_types = parse_test_types(opts[:required_types])
|
|
47
|
+
|
|
44
48
|
project_id = resolve_project_id(opts[:project])
|
|
45
49
|
|
|
46
50
|
# Parse required surfaces (comma-separated)
|
|
@@ -56,6 +60,7 @@ module Fp
|
|
|
56
60
|
project_id: project_id,
|
|
57
61
|
required_surfaces: required_surfaces
|
|
58
62
|
}
|
|
63
|
+
params[:required_test_types] = required_test_types unless required_test_types.empty?
|
|
59
64
|
params[:why] = opts[:why] if opts[:why]
|
|
60
65
|
params[:acceptance] = opts[:acceptance] if opts[:acceptance]
|
|
61
66
|
params[:category] = opts[:category] if opts[:category]
|
|
@@ -94,6 +99,9 @@ module Fp
|
|
|
94
99
|
puts " Category: #{requirement['category'] || '(none)'}"
|
|
95
100
|
puts " Tags: #{(requirement['tags'] || []).join(', ')}" unless (requirement['tags'] || []).empty?
|
|
96
101
|
puts " Surfaces: #{(requirement['required_surfaces'] || []).join(', ')}"
|
|
102
|
+
unless (requirement['required_test_types'] || []).empty?
|
|
103
|
+
puts " Test types: #{(requirement['required_test_types'] || []).join(', ')} (each must have evidence on every surface)"
|
|
104
|
+
end
|
|
97
105
|
puts " Parent: #{opts[:parent] || '(none)'}" if opts[:parent]
|
|
98
106
|
puts
|
|
99
107
|
puts '⚠️ This requirement is a DRAFT.'
|
|
@@ -104,6 +112,19 @@ module Fp
|
|
|
104
112
|
|
|
105
113
|
private
|
|
106
114
|
|
|
115
|
+
# Parse a comma-separated --required-types list into a normalized array. Returns []
|
|
116
|
+
# when the flag is absent (meaning "any test counts / inherit project default").
|
|
117
|
+
#
|
|
118
|
+
# No client-side enum check: valid test types are the *project's* vocabulary
|
|
119
|
+
# (Project#test_type_vocabulary), which the CLI doesn't know without a round-trip.
|
|
120
|
+
# The server validates against that list and returns a verbatim 422 the caller
|
|
121
|
+
# sees — the same way an unknown surface is handled.
|
|
122
|
+
def parse_test_types(raw)
|
|
123
|
+
return [] if raw.nil? || raw.to_s.strip.empty?
|
|
124
|
+
|
|
125
|
+
raw.split(',').map { |t| t.strip.downcase }.reject(&:empty?).uniq
|
|
126
|
+
end
|
|
127
|
+
|
|
107
128
|
# Resolve a parent requirement slug to its ID
|
|
108
129
|
def resolve_parent_id(project_id, parent_slug)
|
|
109
130
|
result = client.list_requirements(project_id: project_id)
|
|
@@ -12,7 +12,7 @@ module Fp
|
|
|
12
12
|
class ProposeChange < Base
|
|
13
13
|
KNOWN_FLAGS = {
|
|
14
14
|
project: :string, name: :string, why: :string, acceptance: :string,
|
|
15
|
-
required: :string, category: :string, parent: :string, slug: :string,
|
|
15
|
+
required: :string, required_types: :string, category: :string, parent: :string, slug: :string,
|
|
16
16
|
reason: :string, source_type: :string, source_url: :string, source_context: :string,
|
|
17
17
|
removal: :boolean
|
|
18
18
|
}.freeze
|
|
@@ -48,8 +48,8 @@ module Fp
|
|
|
48
48
|
removal = opts[:removal] ? true : false
|
|
49
49
|
|
|
50
50
|
if !removal && params.keys.none? { |k| proposed_field?(k) }
|
|
51
|
-
output.error('Nothing to propose. Pass at least one of --name/--why/--acceptance/' \
|
|
52
|
-
'--required/--category/--parent, or --removal to propose deletion.')
|
|
51
|
+
output.error('Nothing to propose. Pass at least one of --name/--slug/--why/--acceptance/' \
|
|
52
|
+
'--required/--required-types/--category/--parent, or --removal to propose deletion.')
|
|
53
53
|
exit 1
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -70,6 +70,10 @@ module Fp
|
|
|
70
70
|
exit 1
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# No client-side test-type check: the valid set is the project's own vocabulary
|
|
74
|
+
# (Project#test_type_vocabulary), which the CLI can't know without a round-trip.
|
|
75
|
+
# The server validates and returns a verbatim 422.
|
|
76
|
+
|
|
73
77
|
return unless opts[:source_type] && !VALID_SOURCE_TYPES.include?(opts[:source_type])
|
|
74
78
|
|
|
75
79
|
output.error("Invalid source type '#{opts[:source_type]}'. Must be one of: #{VALID_SOURCE_TYPES.join(', ')}")
|
|
@@ -90,6 +94,9 @@ module Fp
|
|
|
90
94
|
|
|
91
95
|
FIELD_FLAGS.each { |flag, field| params[field] = opts[flag] if opts[flag] }
|
|
92
96
|
params[:required_surfaces] = opts[:required].split(',').map(&:strip) if opts[:required]
|
|
97
|
+
if opts[:required_types]
|
|
98
|
+
params[:required_test_types] = opts[:required_types].split(',').map { |t| t.strip.downcase }.reject(&:empty?)
|
|
99
|
+
end
|
|
93
100
|
params[:parent_id] = resolve_parent_id(project_id, opts[:parent]) if opts[:parent]
|
|
94
101
|
|
|
95
102
|
params[:removal] = true if opts[:removal]
|
|
@@ -102,7 +109,7 @@ module Fp
|
|
|
102
109
|
end
|
|
103
110
|
|
|
104
111
|
def proposed_field?(key)
|
|
105
|
-
%i[title why acceptance category slug required_surfaces parent_id].include?(key)
|
|
112
|
+
%i[title why acceptance category slug required_surfaces required_test_types parent_id].include?(key)
|
|
106
113
|
end
|
|
107
114
|
|
|
108
115
|
def resolve_parent_id(project_id, parent_slug)
|
|
@@ -136,11 +143,12 @@ module Fp
|
|
|
136
143
|
print_diff(change['changes'])
|
|
137
144
|
print_source(change)
|
|
138
145
|
|
|
146
|
+
current_slug = requirement['slug'].to_s.empty? ? slug : requirement['slug']
|
|
139
147
|
puts " Status: #{requirement['status']}"
|
|
140
148
|
puts " Change: #{change['id']}"
|
|
141
149
|
puts
|
|
142
150
|
puts '⚠️ This requirement is back in DRAFT pending human review.'
|
|
143
|
-
puts " Run `fp history #{
|
|
151
|
+
puts " Run `fp history #{current_slug} --project <project>` to see the full diff."
|
|
144
152
|
puts ' A human must approve or reject it in the web app. Agents cannot review changes.'
|
|
145
153
|
end
|
|
146
154
|
end
|
data/lib/fp/commands/report.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Fp
|
|
|
18
18
|
KNOWN_FLAGS = {
|
|
19
19
|
project: :string,
|
|
20
20
|
surface: :string,
|
|
21
|
+
test_type: :string,
|
|
21
22
|
file: :string,
|
|
22
23
|
line: :string,
|
|
23
24
|
repo: :string,
|
|
@@ -57,6 +58,10 @@ module Fp
|
|
|
57
58
|
exit 1
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
# No client-side test-type check: the valid set is the project's own vocabulary,
|
|
62
|
+
# which the CLI can't know without a round-trip. The server validates and returns
|
|
63
|
+
# a verbatim 422.
|
|
64
|
+
|
|
60
65
|
project_id = resolve_project_id(opts[:project])
|
|
61
66
|
|
|
62
67
|
# Verify the requirement exists before reporting evidence
|
|
@@ -75,6 +80,7 @@ module Fp
|
|
|
75
80
|
repo: opts[:repo],
|
|
76
81
|
state: state
|
|
77
82
|
}
|
|
83
|
+
params[:test_type] = opts[:test_type] if opts[:test_type]
|
|
78
84
|
params[:pr_url] = opts[:pr] if opts[:pr]
|
|
79
85
|
params[:sha] = opts[:sha] if opts[:sha]
|
|
80
86
|
params[:work_item_url] = opts[:work_item] if opts[:work_item]
|
|
@@ -112,6 +118,7 @@ module Fp
|
|
|
112
118
|
output.success(result[:data]) do
|
|
113
119
|
puts "Evidence reported for '#{slug}' on surface '#{opts[:surface]}'."
|
|
114
120
|
puts " State: #{state}"
|
|
121
|
+
puts " Type: #{opts[:test_type]}" if opts[:test_type]
|
|
115
122
|
puts " File: #{opts[:file]}#{opts[:line] ? ":#{opts[:line]}" : ''}"
|
|
116
123
|
if opts[:pr]
|
|
117
124
|
puts " PR: #{opts[:pr]}"
|
|
@@ -150,6 +157,9 @@ module Fp
|
|
|
150
157
|
exit 1
|
|
151
158
|
end
|
|
152
159
|
|
|
160
|
+
# No client-side test-type check — the server validates --test-type / marker
|
|
161
|
+
# types against the project's own vocabulary and returns a verbatim 422.
|
|
162
|
+
|
|
153
163
|
begin
|
|
154
164
|
testcases = Fp::JUnit.parse_file(opts[:junit])
|
|
155
165
|
rescue ArgumentError => e
|
|
@@ -163,7 +173,11 @@ module Fp
|
|
|
163
173
|
end
|
|
164
174
|
|
|
165
175
|
base_dir = opts[:base_dir] || Dir.pwd
|
|
166
|
-
|
|
176
|
+
# --test-type is the default kind for markers that pin none (`fp:slug`,
|
|
177
|
+
# `fp:slug@api`); a marker's own `:type` suffix always wins.
|
|
178
|
+
result = Fp::JUnit.bind_markers(testcases, base_dir: base_dir,
|
|
179
|
+
default_surface: opts[:surface],
|
|
180
|
+
default_test_type: opts[:test_type])
|
|
167
181
|
bindings = result[:bindings]
|
|
168
182
|
unmatched = result[:unmatched]
|
|
169
183
|
missing_surface = result[:missing_surface] || []
|
|
@@ -207,6 +221,7 @@ module Fp
|
|
|
207
221
|
repo: opts[:repo],
|
|
208
222
|
state: state
|
|
209
223
|
}
|
|
224
|
+
params[:test_type] = binding.test_type if binding.test_type
|
|
210
225
|
params[:line] = binding.line if binding.line
|
|
211
226
|
params[:pr_url] = opts[:pr] if opts[:pr]
|
|
212
227
|
params[:sha] = opts[:sha] if opts[:sha]
|
|
@@ -238,7 +253,7 @@ module Fp
|
|
|
238
253
|
def emit_junit_summary(opts, reported, skipped_unknown, unmatched, api_unavailable)
|
|
239
254
|
data = {
|
|
240
255
|
repo: opts[:repo],
|
|
241
|
-
reported: reported.map { |r| { slug: r[:binding].slug, surface: r[:binding].surface, file: r[:binding].file, line: r[:binding].line, state: r[:state], recorded: r[:recorded] } },
|
|
256
|
+
reported: reported.map { |r| { slug: r[:binding].slug, surface: r[:binding].surface, test_type: r[:binding].test_type, file: r[:binding].file, line: r[:binding].line, state: r[:state], recorded: r[:recorded] } },
|
|
242
257
|
skipped_unknown: skipped_unknown.map { |b| { slug: b.slug, surface: b.surface, file: b.file } },
|
|
243
258
|
unmatched_testcases: unmatched.map { |tc| { name: tc.name, file: tc.file } }
|
|
244
259
|
}
|
|
@@ -248,7 +263,8 @@ module Fp
|
|
|
248
263
|
reported.each do |r|
|
|
249
264
|
marker = r[:recorded] ? '✓' : '•'
|
|
250
265
|
loc = r[:binding].line ? "#{r[:binding].file}:#{r[:binding].line}" : r[:binding].file
|
|
251
|
-
|
|
266
|
+
type = r[:binding].test_type ? " [#{r[:binding].test_type}]" : ''
|
|
267
|
+
puts " #{marker} #{r[:binding].slug} → #{r[:binding].surface}#{type} (#{r[:state]}) #{loc}"
|
|
252
268
|
end
|
|
253
269
|
|
|
254
270
|
unless skipped_unknown.empty?
|
data/lib/fp/commands/show.rb
CHANGED
|
@@ -81,6 +81,19 @@ module Fp
|
|
|
81
81
|
end
|
|
82
82
|
puts
|
|
83
83
|
|
|
84
|
+
# Required test types: the kinds of test that must each cover this requirement on
|
|
85
|
+
# every surface. Empty means "any test counts" (or it inherits the project
|
|
86
|
+
# default) — shown explicitly so an agent knows whether one test is enough.
|
|
87
|
+
test_types = req['required_test_types'] || []
|
|
88
|
+
puts 'Required Test Types:'
|
|
89
|
+
if test_types.empty?
|
|
90
|
+
puts ' (any — a single test of any kind satisfies each surface)'
|
|
91
|
+
else
|
|
92
|
+
test_types.each { |t| puts " - #{t}" }
|
|
93
|
+
puts ' (each kind needs its own evidence on every required surface)'
|
|
94
|
+
end
|
|
95
|
+
puts
|
|
96
|
+
|
|
84
97
|
if req['why'] && !req['why'].empty?
|
|
85
98
|
puts 'Why:'
|
|
86
99
|
puts " #{req['why']}"
|
|
@@ -107,6 +120,7 @@ module Fp
|
|
|
107
120
|
puts " #{surface}:"
|
|
108
121
|
records.each do |e|
|
|
109
122
|
puts " Status: #{e['status']}"
|
|
123
|
+
puts " Type: #{e['test_type']}" if e['test_type'] && e['test_type'] != 'unspecified'
|
|
110
124
|
if e['source_file']
|
|
111
125
|
loc = e['source_line'] ? "#{e['source_file']}:#{e['source_line']}" : e['source_file']
|
|
112
126
|
puts " File: #{loc}"
|
data/lib/fp/commands.rb
CHANGED
|
@@ -21,6 +21,9 @@ require_relative 'commands/setup'
|
|
|
21
21
|
require_relative 'commands/whoami'
|
|
22
22
|
require_relative 'commands/version'
|
|
23
23
|
require_relative 'commands/help'
|
|
24
|
+
require_relative 'commands/attach'
|
|
25
|
+
require_relative 'commands/attachments'
|
|
26
|
+
require_relative 'commands/conversation'
|
|
24
27
|
|
|
25
28
|
module Fp
|
|
26
29
|
module Commands
|