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.
@@ -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
- # --why must be brief and actually explain why: whoever approves it should grasp
157
- # in one read the user need or outcome the feature serves. Longer is fine when it
158
- # earns its length; wordy is not. Avoid filler, and avoid describing HOW it works
159
- # instead of WHY anyone wants it.
160
- # good: "Staff scan the label to find a container; without a QR they key the ID by hand and mis-type it."
161
- # bad: "Ensures a high-quality experience across all surfaces." (filler)
162
- # --acceptance is the observable condition that proves it's done, not a
163
- # restatement of the title.
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 \\
@@ -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 run `fp list` / `fp show` to find
294
- the requirement it belongs to. If one exists, you'll bind your test to it; if none
295
- does, propose one (as a draft) so the work is on the record from the outset. Treating
296
- this as the opening move — not a chore at the end — is what keeps parity honest.
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 requirement — do this first
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
- If no requirement covers what you're building, propose one. Agents always create
308
- **drafts**; a human activates them:
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
- **Writing the `--why`: brief, and it actually explains why.** Someone approving
317
- this requirement needs to grasp in one read why the product needs it — the user
318
- problem it solves or the outcome it protects. State that and stop. Longer is fine
319
- when it earns its length; wordy is not. Avoid two things: filler that says nothing
320
- specific, and describing *how* the feature works instead of *why* anyone wants it.
321
-
322
- - ✅ `--why "Warehouse staff scan the label to look up a container; without a QR code they key in the ID by hand and mis-type it."`
323
- - ❌ `--why "This is an important requirement that ensures a high-quality experience for our users across all surfaces."` (filler — says nothing specific)
324
- - ❌ `--why "The label renders a QR image encoding the container ID at print time."` (how it works, not why anyone needs it)
325
-
326
- Same rule for `--acceptance`: state the observable condition that proves it's done
327
- (`"QR scans to the container's detail page"`), not a restatement of the title.
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 cannot change `--slug` (it's the join to every `fp:<slug>` marker) or remove a
351
- required surface. Both come back as 403.
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
@@ -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 (immutable identifier for the requirement)')
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 #{slug} --project <project>` to see the full diff."
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
@@ -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
- result = Fp::JUnit.bind_markers(testcases, base_dir: base_dir, default_surface: opts[:surface])
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
- puts " #{marker} #{r[:binding].slug} → #{r[:binding].surface} (#{r[:state]}) #{loc}"
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?
@@ -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