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/junit.rb
CHANGED
|
@@ -32,8 +32,13 @@ module Fp
|
|
|
32
32
|
# One requirement's aggregated CI outcome for one surface, ready to hand to the
|
|
33
33
|
# backend's ingest endpoint. This is what lets CI upload *resolved* results rather
|
|
34
34
|
# than raw XML the server would have to re-scrape the repo to understand.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
#
|
|
36
|
+
# `title` names a representative test — the first one that *failed* when the verdict
|
|
37
|
+
# is `failing`, otherwise the first that ran. Without it the evidence row keeps
|
|
38
|
+
# whatever title an earlier agent report left behind, and the web app then blames a
|
|
39
|
+
# test that is perfectly green for the failure.
|
|
40
|
+
CiResult = Struct.new(:slug, :surface, :test_type, :status, :file, :line, :title, :test_count,
|
|
41
|
+
:failure_count, keyword_init: true)
|
|
37
42
|
|
|
38
43
|
# A failing testcase that carries no fp:<slug> marker at all — a "loose end". Not a
|
|
39
44
|
# cell on the parity grid (nothing binds it to a requirement) and not an orphan
|
|
@@ -47,25 +52,37 @@ module Fp
|
|
|
47
52
|
# know *which* test the marker sits above (matched by line or by test name) or only
|
|
48
53
|
# that the marker is somewhere in the same file — a title is only worth reporting in
|
|
49
54
|
# the first case.
|
|
50
|
-
|
|
55
|
+
#
|
|
56
|
+
# `repo_file` is the testcase's source file expressed relative to the *repository
|
|
57
|
+
# root*, which is what gets reported. The path in the report is relative to whatever
|
|
58
|
+
# directory the runner was invoked from (vitest reports `src/pages/Foo.test.jsx` from
|
|
59
|
+
# inside `frontend/`), so reporting it verbatim produces a link to a file that doesn't
|
|
60
|
+
# exist at that path in the repo.
|
|
61
|
+
Match = Struct.new(:testcase, :marker, :precise, :repo_file, keyword_init: true)
|
|
51
62
|
|
|
52
63
|
# Evidence discovered by binding a testcase to a marker.
|
|
53
64
|
# `line` is the 1-based line number of the test in its source file: the
|
|
54
65
|
# testcase's own line when the runner reported one, otherwise the line of
|
|
55
66
|
# the fp:<slug> marker that annotates it (markers sit directly above the
|
|
56
67
|
# test, so the marker line is a close stand-in when the runner omits lines).
|
|
57
|
-
Binding = Struct.new(:slug, :surface, :file, :line, :title, :state, keyword_init: true)
|
|
68
|
+
Binding = Struct.new(:slug, :surface, :test_type, :file, :line, :title, :state, keyword_init: true)
|
|
58
69
|
|
|
59
70
|
# Matches `fp:<slug>` inside a comment, with an optional `@surface` (or
|
|
60
|
-
# `@surface1,surface2`) suffix to target one or more specific surfaces
|
|
71
|
+
# `@surface1,surface2`) suffix to target one or more specific surfaces, and an
|
|
72
|
+
# optional `:<type>` suffix to pin the kind of test:
|
|
61
73
|
#
|
|
62
|
-
# fp:print_qr #
|
|
74
|
+
# fp:print_qr # --surface flag default, unspecified type
|
|
63
75
|
# fp:print_qr@api # reports evidence for the `api` surface
|
|
64
76
|
# fp:print_qr@api,web # reports for both `api` and `web`
|
|
77
|
+
# fp:print_qr@api:e2e # the `api` surface, filed as an e2e test
|
|
78
|
+
# fp:print_qr:unit # --surface default, filed as a unit test
|
|
79
|
+
# fp:print_qr@api,web:e2e # both surfaces, each filed as an e2e test
|
|
65
80
|
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
81
|
+
# The type pins the kind of test (unit, integration, e2e, manual, other) so a
|
|
82
|
+
# requirement that demands several kinds can tell them apart. It applies to every
|
|
83
|
+
# surface the marker names. Slugs, surfaces and types are lowercase letters, digits,
|
|
84
|
+
# underscores and hyphens (matching the propose/report convention).
|
|
85
|
+
MARKER_RE = /fp:([a-z0-9][a-z0-9_-]*)(?:@([a-z0-9_-]+(?:,[a-z0-9_-]+)*))?(?::([a-z0-9_-]+))?/.freeze
|
|
69
86
|
|
|
70
87
|
# A marker only counts when it is a comment on its own line — the documented
|
|
71
88
|
# convention ("the fp:<slug> comment sits immediately above its test"). Requiring a
|
|
@@ -174,7 +191,7 @@ module Fp
|
|
|
174
191
|
# unmatched: [TestCase, ...], # testcases with no discoverable marker
|
|
175
192
|
# missing_surface: [Binding-ish], # markers with no surface and no default
|
|
176
193
|
# }
|
|
177
|
-
def bind_markers(testcases, base_dir: Dir.pwd, default_surface: nil)
|
|
194
|
+
def bind_markers(testcases, base_dir: Dir.pwd, default_surface: nil, default_test_type: nil)
|
|
178
195
|
bindings = {}
|
|
179
196
|
matched = {}
|
|
180
197
|
missing_surface = []
|
|
@@ -187,11 +204,17 @@ module Fp
|
|
|
187
204
|
surfaces = marker[:surfaces]
|
|
188
205
|
surfaces = [default_surface] if surfaces.empty?
|
|
189
206
|
|
|
207
|
+
# The marker's own type pin wins; otherwise the --test-type default (nil → the
|
|
208
|
+
# server files it as `unspecified`).
|
|
209
|
+
test_type = marker[:test_type] || default_test_type
|
|
210
|
+
|
|
190
211
|
surfaces.each do |surface|
|
|
191
212
|
if surface.nil? || surface.empty?
|
|
192
213
|
missing_surface << { slug: marker[:slug], file: tc.file }
|
|
193
214
|
else
|
|
194
|
-
record_binding(bindings, marker[:slug], surface, tc, marker,
|
|
215
|
+
record_binding(bindings, marker[:slug], surface, tc, marker,
|
|
216
|
+
titles_reliable: match.precise, file: match.repo_file,
|
|
217
|
+
test_type: test_type)
|
|
195
218
|
end
|
|
196
219
|
end
|
|
197
220
|
end
|
|
@@ -212,7 +235,7 @@ module Fp
|
|
|
212
235
|
# it was skipped (a skipped test is not proof of anything), and `passing` otherwise.
|
|
213
236
|
#
|
|
214
237
|
# Returns { results: [CiResult, ...], unmatched: [TestCase], missing_surface: [...] }.
|
|
215
|
-
def ci_results(testcases, base_dir: Dir.pwd, default_surface: nil)
|
|
238
|
+
def ci_results(testcases, base_dir: Dir.pwd, default_surface: nil, default_test_type: nil)
|
|
216
239
|
aggregates = {}
|
|
217
240
|
matched = {}
|
|
218
241
|
missing_surface = []
|
|
@@ -225,31 +248,47 @@ module Fp
|
|
|
225
248
|
surfaces = marker[:surfaces]
|
|
226
249
|
surfaces = [default_surface] if surfaces.empty?
|
|
227
250
|
|
|
251
|
+
# The marker's own type pin wins; otherwise the --test-type default (nil → the
|
|
252
|
+
# server files it as `unspecified`). One CI verdict per (slug, surface, type).
|
|
253
|
+
test_type = marker[:test_type] || default_test_type
|
|
254
|
+
|
|
228
255
|
surfaces.each do |surface|
|
|
229
256
|
if surface.nil? || surface.empty?
|
|
230
257
|
missing_surface << { slug: marker[:slug], file: tc.file }
|
|
231
258
|
next
|
|
232
259
|
end
|
|
233
260
|
|
|
234
|
-
key = [marker[:slug], surface]
|
|
235
|
-
agg = aggregates[key] ||= { file:
|
|
261
|
+
key = [marker[:slug], surface, test_type]
|
|
262
|
+
agg = aggregates[key] ||= { file: match.repo_file, line: tc.line || marker[:line],
|
|
263
|
+
title: nil, failing_title: nil, ran: 0, failed: 0, skipped: 0 }
|
|
236
264
|
if tc.stub?
|
|
237
265
|
agg[:skipped] += 1
|
|
238
266
|
else
|
|
239
267
|
agg[:ran] += 1
|
|
240
268
|
agg[:failed] += 1 if tc.failed?
|
|
241
269
|
end
|
|
270
|
+
|
|
271
|
+
# Name a test for the evidence row. The first *failing* test is the one a human
|
|
272
|
+
# wants to see when the cell goes red; otherwise the first that ran will do.
|
|
273
|
+
# Only when we know which test the marker annotates — a guess here is worse
|
|
274
|
+
# than no title.
|
|
275
|
+
if match.precise && (title = presence(tc.name))
|
|
276
|
+
agg[:title] ||= title
|
|
277
|
+
agg[:failing_title] ||= title if tc.failed?
|
|
278
|
+
end
|
|
242
279
|
end
|
|
243
280
|
end
|
|
244
281
|
|
|
245
|
-
results = aggregates.map do |(slug, surface), agg|
|
|
282
|
+
results = aggregates.map do |(slug, surface, test_type), agg|
|
|
246
283
|
status =
|
|
247
284
|
if agg[:failed].positive? then 'failing'
|
|
248
285
|
elsif agg[:ran].zero? then 'stub'
|
|
249
286
|
else 'passing'
|
|
250
287
|
end
|
|
251
288
|
|
|
252
|
-
CiResult.new(slug: slug, surface: surface,
|
|
289
|
+
CiResult.new(slug: slug, surface: surface, test_type: test_type, status: status,
|
|
290
|
+
file: agg[:file], line: agg[:line],
|
|
291
|
+
title: agg[:failing_title] || agg[:title],
|
|
253
292
|
test_count: agg[:ran], failure_count: agg[:failed])
|
|
254
293
|
end
|
|
255
294
|
|
|
@@ -274,21 +313,66 @@ module Fp
|
|
|
274
313
|
# can never disagree about which test a marker annotates.
|
|
275
314
|
def walk_matches(testcases, base_dir)
|
|
276
315
|
marker_cache = {}
|
|
316
|
+
root = repo_root_for(base_dir)
|
|
277
317
|
|
|
278
318
|
# Group testcases by their source file so we can reason about ordering.
|
|
279
|
-
testcases.group_by(&:file).each do |
|
|
319
|
+
testcases.group_by(&:file).each do |file, cases|
|
|
280
320
|
markers = markers_for(cases.first, base_dir, marker_cache)
|
|
281
321
|
next if markers.empty?
|
|
282
322
|
|
|
323
|
+
repo_file = repo_relative_path(file, base_dir, root)
|
|
324
|
+
|
|
283
325
|
cases.each do |tc|
|
|
284
326
|
marker, precise = marker_for(tc, cases, markers)
|
|
285
327
|
next unless marker
|
|
286
328
|
|
|
287
|
-
yield Match.new(testcase: tc, marker: marker, precise: precise)
|
|
329
|
+
yield Match.new(testcase: tc, marker: marker, precise: precise, repo_file: repo_file)
|
|
288
330
|
end
|
|
289
331
|
end
|
|
290
332
|
end
|
|
291
333
|
|
|
334
|
+
# The repository root containing `dir`, or nil when `dir` isn't inside one. #
|
|
335
|
+
# Found by walking up for a `.git` entry rather than shelling out to git, so it also
|
|
336
|
+
# works in a worktree (where `.git` is a file) and costs nothing.
|
|
337
|
+
def repo_root_for(dir)
|
|
338
|
+
current = File.expand_path(dir)
|
|
339
|
+
|
|
340
|
+
loop do
|
|
341
|
+
return current if File.exist?(File.join(current, '.git'))
|
|
342
|
+
|
|
343
|
+
parent = File.dirname(current)
|
|
344
|
+
return nil if parent == current
|
|
345
|
+
|
|
346
|
+
current = parent
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Express a report's test-file path relative to the repository root.
|
|
351
|
+
#
|
|
352
|
+
# The path in a JUnit report is relative to wherever the runner ran, which is not
|
|
353
|
+
# necessarily the repo root — vitest invoked in `frontend/` reports
|
|
354
|
+
# `src/pages/Matrix.test.jsx`, and `--base-dir frontend` is what makes that resolvable
|
|
355
|
+
# locally. Evidence is rendered as a GitHub link built from the repo plus this path,
|
|
356
|
+
# so reporting it verbatim links to a path that doesn't exist (a 404).
|
|
357
|
+
#
|
|
358
|
+
# Falls back to the path as given when there's no repo to relativise against, or when
|
|
359
|
+
# the file resolves outside it.
|
|
360
|
+
def repo_relative_path(file, base_dir, root = repo_root_for(base_dir))
|
|
361
|
+
return file if file.nil? || file.empty? || root.nil?
|
|
362
|
+
|
|
363
|
+
absolute = File.expand_path(file, base_dir)
|
|
364
|
+
prefix = "#{root}#{File::SEPARATOR}"
|
|
365
|
+
return file unless absolute.start_with?(prefix)
|
|
366
|
+
|
|
367
|
+
absolute.delete_prefix(prefix)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# nil for blank, the stripped value otherwise.
|
|
371
|
+
def presence(value)
|
|
372
|
+
stripped = value.to_s.strip
|
|
373
|
+
stripped.empty? ? nil : stripped
|
|
374
|
+
end
|
|
375
|
+
|
|
292
376
|
# Determine the marker (slug + surfaces) that annotates a testcase.
|
|
293
377
|
# Returns [marker, precise] — or [nil, false] when nothing can be tied to it.
|
|
294
378
|
def marker_for(testcase, sibling_cases, markers)
|
|
@@ -363,9 +447,12 @@ module Fp
|
|
|
363
447
|
candidate
|
|
364
448
|
end
|
|
365
449
|
|
|
366
|
-
def record_binding(bindings, slug, surface, testcase, marker, titles_reliable: true
|
|
367
|
-
|
|
368
|
-
|
|
450
|
+
def record_binding(bindings, slug, surface, testcase, marker, titles_reliable: true, file: nil,
|
|
451
|
+
test_type: nil)
|
|
452
|
+
rel_file = file || testcase.file
|
|
453
|
+
# test_type is part of the key: a unit test and an e2e test for the same
|
|
454
|
+
# slug/surface/file are distinct evidence rows, so they mustn't collapse into one.
|
|
455
|
+
key = [slug, surface, test_type, rel_file]
|
|
369
456
|
state = testcase.stub? ? 'stub' : 'present'
|
|
370
457
|
|
|
371
458
|
# The test's line: prefer the runner-reported testcase line; fall back to
|
|
@@ -377,8 +464,8 @@ module Fp
|
|
|
377
464
|
# Prefer a concrete 'present' state over 'stub' when a slug has both.
|
|
378
465
|
return if existing && !(existing.state == 'stub' && state == 'present')
|
|
379
466
|
|
|
380
|
-
bindings[key] = Binding.new(slug: slug, surface: surface,
|
|
381
|
-
title: (testcase.name if titles_reliable), state: state)
|
|
467
|
+
bindings[key] = Binding.new(slug: slug, surface: surface, test_type: test_type, file: rel_file,
|
|
468
|
+
line: line, title: (testcase.name if titles_reliable), state: state)
|
|
382
469
|
end
|
|
383
470
|
|
|
384
471
|
# Load and cache the fp:<slug> markers (with line numbers) for a testcase's
|
|
@@ -400,9 +487,10 @@ module Fp
|
|
|
400
487
|
cache[file] = markers
|
|
401
488
|
end
|
|
402
489
|
|
|
403
|
-
# Scan file contents for fp:<slug>[@surface[,surface...]] markers, returning
|
|
404
|
-
# [{ slug:, surfaces: [..], line:, title:, title_kind: }, ...]
|
|
405
|
-
# `surfaces` is [] when the marker pins none (use the default
|
|
490
|
+
# Scan file contents for fp:<slug>[@surface[,surface...]][:type] markers, returning
|
|
491
|
+
# [{ slug:, surfaces: [..], test_type:, line:, title:, title_kind: }, ...]
|
|
492
|
+
# (1-based line numbers). `surfaces` is [] when the marker pins none (use the default
|
|
493
|
+
# surface). `test_type` is nil when the marker pins none (filed as `unspecified`).
|
|
406
494
|
#
|
|
407
495
|
# `title` is the name of the test (or describe block) declared below the marker, used
|
|
408
496
|
# to bind markers to results from runners that report no line numbers. It is nil when
|
|
@@ -415,10 +503,11 @@ module Fp
|
|
|
415
503
|
lines.each_with_index do |line, idx|
|
|
416
504
|
next unless line.match?(COMMENT_LINE_RE)
|
|
417
505
|
|
|
418
|
-
line.scan(MARKER_RE) do |(slug, surface_list)|
|
|
506
|
+
line.scan(MARKER_RE) do |(slug, surface_list, test_type)|
|
|
419
507
|
surfaces = surface_list ? surface_list.split(',').map(&:strip).reject(&:empty?) : []
|
|
420
508
|
title, kind = declared_test_after(lines, idx)
|
|
421
|
-
markers << { slug: slug, surfaces: surfaces,
|
|
509
|
+
markers << { slug: slug, surfaces: surfaces, test_type: presence(test_type),
|
|
510
|
+
line: idx + 1, title: title, title_kind: kind }
|
|
422
511
|
end
|
|
423
512
|
end
|
|
424
513
|
markers
|
data/lib/fp/version.rb
CHANGED
data/skills/fp/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fp
|
|
3
|
-
description: "Use the fp CLI
|
|
3
|
+
description: "Use the fp CLI as a complete product specification: write decision-ready requirements, inventory and decompose scope, manage changes through review, bind tests to focused promises, report evidence, and inspect parity."
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Requires the fp executable on PATH (gem install featureparity)
|
|
6
6
|
metadata:
|
|
@@ -41,17 +41,50 @@ If not set, ask the user for their API key or have them run `fp setup`.
|
|
|
41
41
|
|
|
42
42
|
## Core workflows
|
|
43
43
|
|
|
44
|
-
### 1.
|
|
44
|
+
### 1. Inventory and model the complete requirement scope
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
fp list --project stowzilla # All active requirements
|
|
48
|
+
fp list --project stowzilla --status draft # Proposed work; avoid duplicates
|
|
48
49
|
fp list --project stowzilla --gaps # Requirements with a required surface lacking evidence
|
|
49
|
-
fp show <slug> --project stowzilla # Requirement details
|
|
50
|
+
fp show <slug> --project stowzilla # Requirement details and evidence
|
|
51
|
+
fp history <slug> --project stowzilla # Origins and pending changes
|
|
50
52
|
fp matrix --project stowzilla # ASCII parity matrix
|
|
51
53
|
fp matrix --project stowzilla --csv # Export as CSV
|
|
52
54
|
fp check --project stowzilla # Enforce parity (exits 1 on violations)
|
|
53
55
|
```
|
|
54
56
|
|
|
57
|
+
Treat FeatureParity as the human-readable product specification, not a bookkeeping step.
|
|
58
|
+
Before coding, inventory the entire request: user-visible behaviours, policies, limits,
|
|
59
|
+
plans, deadlines, state transitions, permissions, failure modes, and affected surfaces.
|
|
60
|
+
Compare all of it with active and draft requirements, including adjacent requirements the
|
|
61
|
+
change might make inaccurate.
|
|
62
|
+
|
|
63
|
+
Model that scope deliberately:
|
|
64
|
+
|
|
65
|
+
- One independently testable or independently failable product promise per focused
|
|
66
|
+
requirement. Split an acceptance sentence when its clauses could pass or fail separately.
|
|
67
|
+
- Use a parent for a broad capability and children for distinct behaviours, policies, or
|
|
68
|
+
surface-specific promises. Do not model implementation details with no product promise.
|
|
69
|
+
- Record concrete decisions such as prices, quotas, dates, roles, and fallbacks in acceptance
|
|
70
|
+
criteria rather than leaving them as assumptions in code or a pull request.
|
|
71
|
+
- Propose changes to existing requirements whose meaning moved; adding a new requirement is
|
|
72
|
+
not enough if it leaves an old one false.
|
|
73
|
+
- Reconcile again before finishing: every shipped behaviour maps to a current requirement,
|
|
74
|
+
every changed promise is recorded, and every focused requirement has marked evidence or
|
|
75
|
+
remains an explicit gap. Never let one broad marker imply coverage of untested clauses.
|
|
76
|
+
|
|
77
|
+
**Prevent contradictions before they enter the catalog.** Before proposing a new or changed
|
|
78
|
+
promise, compare its intended acceptance criteria with related active and draft requirements
|
|
79
|
+
and read their history. Ask whether both promises can be true at the same time for the same
|
|
80
|
+
user, state, and conditions. If not, do not add another requirement.
|
|
81
|
+
|
|
82
|
+
A failing test bound to an existing requirement, or an implementation that only works by
|
|
83
|
+
changing one, is a contradiction signal — not permission to weaken the test or write a second
|
|
84
|
+
requirement around it. Identify which promise matches product intent, then use
|
|
85
|
+
`fp propose-change` (or `--removal`) with the source and reason to put that decision before a
|
|
86
|
+
human. If the intent is unclear, stop and ask rather than making the catalog say both things.
|
|
87
|
+
|
|
55
88
|
### 2. Add the fp:<slug> marker to tests
|
|
56
89
|
|
|
57
90
|
Place a comment immediately before the test to bind it to a requirement:
|
|
@@ -106,31 +139,82 @@ fp report --junit junit.xml \
|
|
|
106
139
|
- `--surface` is the default; markers can override with `@suffix`
|
|
107
140
|
- `--base-dir DIR` sets where relative paths resolve from
|
|
108
141
|
|
|
109
|
-
### 5.
|
|
142
|
+
### 5. Write and propose decision-ready requirements
|
|
143
|
+
|
|
144
|
+
A requirement is a product decision a reviewer can approve, not a design task for an
|
|
145
|
+
engineer to carry out. It should sound like a capable teammate wrote it: natural, direct,
|
|
146
|
+
and specific enough to protect an outcome without dictating an accidental solution.
|
|
147
|
+
|
|
148
|
+
Treat the catalog as the current record of shared understanding, not a finished specification
|
|
149
|
+
handed from a product owner to implementers. Requirements are discovered and refined through
|
|
150
|
+
ongoing conversation among customers, domain experts, and builders. The human product owner
|
|
151
|
+
makes the final value and priority decisions; agents participate by exposing gaps,
|
|
152
|
+
inconsistencies, alternatives, and useful splits. Prefer customer outcomes over devotion to a
|
|
153
|
+
feature list.
|
|
154
|
+
|
|
155
|
+
Before drafting:
|
|
156
|
+
|
|
157
|
+
- Learn from the request, related requirements and history, support reports, research, and
|
|
158
|
+
observed user behaviour when available. Look for what people attempt, where they abandon a
|
|
159
|
+
task, what they actually use, and what experiments reveal. That evidence may expose a
|
|
160
|
+
different need from the first solution they suggested.
|
|
161
|
+
- Separate facts and decided policy from open questions. Never turn a missing decision into
|
|
162
|
+
a plausible-sounding price, quota, timeout, count, date, role, or fallback. Ask when the
|
|
163
|
+
answer changes the product promise.
|
|
164
|
+
- Identify the actor, the situation that triggers the need, the valuable outcome, and material
|
|
165
|
+
boundaries such as permissions, failures, and affected surfaces.
|
|
166
|
+
- Keep one independently failable promise per requirement. Use a parent and focused children
|
|
167
|
+
when a capability contains several decisions.
|
|
168
|
+
|
|
169
|
+
Write each field for a reviewer, not for the implementation:
|
|
170
|
+
|
|
171
|
+
- **Title:** one clear product promise in present tense. Name the capability or outcome, not
|
|
172
|
+
the project (`Dispatchers see current shipment status`, not `Implement status polling`).
|
|
173
|
+
- **Why:** one or two natural sentences saying who needs this, the problem or risk, and the
|
|
174
|
+
outcome worth protecting. Do not repeat the title, advertise the feature, or explain how
|
|
175
|
+
it will be built. Delete AI filler such as “ensure a seamless, robust experience.”
|
|
176
|
+
- **Acceptance:** observable behaviour that lets someone decide whether the promise is true.
|
|
177
|
+
State the relevant trigger or situation, the result, and material boundaries. Include
|
|
178
|
+
failure behaviour, permissions, or cross-surface consistency when those affect the promise.
|
|
179
|
+
- **Examples:** use concrete examples or Given/When/Then cases when they clarify a rule or an
|
|
180
|
+
edge. Examples and executable tests ground and double-check the specification; they do not
|
|
181
|
+
define every case or replace the general rule and continued conversation.
|
|
182
|
+
- **Numbers and policy:** include a number only when the source made it a real product decision
|
|
183
|
+
or an external constraint requires it. Do not invent implementation defaults and present
|
|
184
|
+
them as scope.
|
|
185
|
+
- **Implementation detail:** omit components, data models, algorithms, endpoints, polling
|
|
186
|
+
intervals, and UI controls unless that interface or constraint is itself user-visible policy.
|
|
187
|
+
|
|
188
|
+
A good requirement is concise, not merely short. Keep every sentence that changes a decision;
|
|
189
|
+
remove everything that only sounds thorough.
|
|
190
|
+
|
|
191
|
+
- ✅ **Title:** `Dispatchers see current shipment status`
|
|
192
|
+
**Why:** `Dispatchers need to spot delayed shipments before they miss a delivery window.`
|
|
193
|
+
**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.`
|
|
194
|
+
- ❌ **Title:** `Implement Redis-backed 30-second status polling`
|
|
195
|
+
**Why:** `This important feature ensures a seamless, robust experience.`
|
|
196
|
+
**Acceptance:** `Use a background worker and refresh every 30 seconds.`
|
|
197
|
+
This invents policy, prescribes a design, and never states the outcome.
|
|
198
|
+
|
|
199
|
+
Before saving, read the draft as a manager deciding scope: Is the value clear? Can the result
|
|
200
|
+
be observed? Are real product decisions distinguished from guesses? Could every clause fail
|
|
201
|
+
independently and, if so, should it be split? Does every sentence earn its place? Revise or ask
|
|
202
|
+
before creating the record if the answer is no.
|
|
203
|
+
|
|
204
|
+
Then create the proposal as a draft:
|
|
110
205
|
|
|
111
206
|
```bash
|
|
112
207
|
fp propose --project stowzilla \
|
|
113
|
-
--slug
|
|
114
|
-
--name "
|
|
115
|
-
--why "
|
|
116
|
-
--required api,
|
|
117
|
-
--acceptance "
|
|
208
|
+
--slug current_shipment_status \
|
|
209
|
+
--name "Dispatchers see current shipment status" \
|
|
210
|
+
--why "Dispatchers need to spot delayed shipments before they miss a delivery window" \
|
|
211
|
+
--required api,web \
|
|
212
|
+
--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" \
|
|
213
|
+
--source-type fizzy \
|
|
214
|
+
--source-url https://app.fizzy.do/123/cards/456
|
|
118
215
|
```
|
|
119
216
|
|
|
120
|
-
**
|
|
121
|
-
requirement should grasp in one read why the product needs it — the user problem it
|
|
122
|
-
solves or the outcome it protects. State that and stop. Longer is fine when it earns
|
|
123
|
-
its length; wordy is not. Avoid two things: filler that says nothing specific, and
|
|
124
|
-
describing *how* the feature works instead of *why* anyone wants it.
|
|
125
|
-
|
|
126
|
-
- ✅ `--why "Staff scan the label to find a container; without a QR they key the ID by hand and mis-type it."`
|
|
127
|
-
- ❌ `--why "Ensures a high-quality experience across all surfaces."` (filler — says nothing specific)
|
|
128
|
-
- ❌ `--why "The label renders a QR image encoding the container ID at print time."` (how it works, not why anyone needs it)
|
|
129
|
-
|
|
130
|
-
Same rule for `--acceptance`: the observable condition that proves it's done, not a
|
|
131
|
-
restatement of the title.
|
|
132
|
-
|
|
133
|
-
**Agents always create requirements as draft.** A human must activate in the web app.
|
|
217
|
+
**Agents always create requirements as draft.** A human must activate them in the web app.
|
|
134
218
|
|
|
135
219
|
### 6. Propose a change to an existing requirement
|
|
136
220
|
|
|
@@ -158,8 +242,9 @@ Rules:
|
|
|
158
242
|
|
|
159
243
|
- Always pass `--reason` and, where one exists, `--source-type`/`--source-url`. The whole
|
|
160
244
|
point of a proposal is that the *why* is recorded next to the *what*.
|
|
161
|
-
- Agents
|
|
162
|
-
|
|
245
|
+
- Agents may correct `--name` (the title) or `--slug` through a proposal. Existing
|
|
246
|
+
evidence follows a proposed slug, but every `fp:<slug>` marker still needs the same
|
|
247
|
+
correction. Agents still cannot remove a required surface.
|
|
163
248
|
- Agents cannot approve or reject. Only humans review.
|
|
164
249
|
- One open proposal per requirement — review the current one before proposing another.
|
|
165
250
|
|
data/skills/fp/references/cli.md
CHANGED
|
@@ -130,36 +130,75 @@ In JUnit mode:
|
|
|
130
130
|
|
|
131
131
|
### fp propose
|
|
132
132
|
|
|
133
|
-
Propose a new requirement as draft.
|
|
133
|
+
Propose a new requirement as a draft. Before proposing, inventory the full request and compare
|
|
134
|
+
it with both active and draft requirements. The catalog should tell a reviewer what the product
|
|
135
|
+
promises, not preserve the first implementation idea somebody mentioned.
|
|
136
|
+
|
|
137
|
+
Use one focused requirement per independently testable or failable product promise. Use a
|
|
138
|
+
parent plus children for a broad capability with distinct behaviours, policies, or
|
|
139
|
+
surface-specific promises. If the work makes an existing requirement inaccurate, use
|
|
140
|
+
`fp propose-change` as well as adding anything new.
|
|
141
|
+
|
|
142
|
+
Before adding or changing a requirement, compare its acceptance criteria with related active
|
|
143
|
+
and draft requirements and their history. Ask whether both promises can be true for the same
|
|
144
|
+
user, state, and conditions. A failing bound test or an implementation that requires changing
|
|
145
|
+
an existing promise is a contradiction signal: decide which promise reflects product intent,
|
|
146
|
+
then record that decision with `fp propose-change` or `--removal`. Do not weaken the test or add
|
|
147
|
+
a second requirement around the conflict; ask a human when product intent is unclear.
|
|
134
148
|
|
|
135
149
|
```bash
|
|
136
150
|
fp propose --project stowzilla \
|
|
137
|
-
--slug
|
|
138
|
-
--name "
|
|
139
|
-
--why "
|
|
140
|
-
--required api,
|
|
141
|
-
--acceptance "
|
|
151
|
+
--slug current_shipment_status \
|
|
152
|
+
--name "Dispatchers see current shipment status" \
|
|
153
|
+
--why "Dispatchers need to spot delayed shipments before they miss a delivery window" \
|
|
154
|
+
--required api,web \
|
|
155
|
+
--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" \
|
|
156
|
+
--source-type fizzy \
|
|
157
|
+
--source-url https://app.fizzy.do/123/cards/456
|
|
142
158
|
```
|
|
143
159
|
|
|
144
160
|
| Flag | Required | Description |
|
|
145
161
|
|------|----------|-------------|
|
|
146
162
|
| `--project` | Yes | Project slug |
|
|
147
|
-
| `--slug` | Yes |
|
|
148
|
-
| `--name` | Yes |
|
|
149
|
-
| `--why` | No |
|
|
163
|
+
| `--slug` | Yes | Initial requirement key (later corrections use `propose-change`) |
|
|
164
|
+
| `--name` | Yes | Short product promise in present tense |
|
|
165
|
+
| `--why` | No | User need, problem or risk, and valuable outcome |
|
|
150
166
|
| `--required` | No | Comma-separated surfaces that must implement this |
|
|
151
|
-
| `--acceptance` | No |
|
|
152
|
-
|
|
153
|
-
**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
167
|
+
| `--acceptance` | No | Observable behaviour and material boundaries |
|
|
168
|
+
|
|
169
|
+
**Write for a decision-maker, not an implementer.** The result should sound natural and be
|
|
170
|
+
ready for management review without someone translating AI boilerplate or reverse-engineering
|
|
171
|
+
the intended outcome. Treat it as the current record of an ongoing conversation, not a finished
|
|
172
|
+
decree from product to engineering. Humans decide value and priority; agents surface gaps,
|
|
173
|
+
inconsistencies, alternatives, and useful splits. Prefer outcomes over feature lists.
|
|
174
|
+
|
|
175
|
+
- **Title:** name the capability or outcome, not the work (`Dispatchers see current shipment
|
|
176
|
+
status`, not `Implement status polling`).
|
|
177
|
+
- **Why:** use one or two natural sentences to say who needs this, the problem or risk, and the
|
|
178
|
+
outcome worth protecting. Do not repeat the title, sell the feature, explain the mechanism,
|
|
179
|
+
or pad it with phrases such as “ensure a seamless, robust experience.”
|
|
180
|
+
- **Acceptance:** state the relevant trigger or situation, the observable result, and material
|
|
181
|
+
boundaries such as permissions, unavailable states, and affected surfaces. Concrete examples
|
|
182
|
+
and tests ground and double-check the rule; they do not define every case or replace the
|
|
183
|
+
general rule and continued conversation.
|
|
184
|
+
- **Numbers and policy:** include prices, quotas, timeouts, counts, dates, roles, and fallbacks
|
|
185
|
+
only when the source made them real product decisions or external constraints. Never invent a
|
|
186
|
+
plausible value to fill silence; ask when the answer changes the promise.
|
|
187
|
+
- **Implementation detail:** omit components, data models, algorithms, endpoints, polling
|
|
188
|
+
intervals, and UI controls unless the interface or constraint is itself part of the promise.
|
|
189
|
+
|
|
190
|
+
Learn from observed user behaviour and other evidence when available: what people attempt,
|
|
191
|
+
where they abandon a task, what they actually use, and what experiments reveal. The first
|
|
192
|
+
proposed solution may point at the need without defining the right scope. Preserve meaningful
|
|
193
|
+
uncertainty as a question instead of laundering a guess into acceptance criteria. Concise does
|
|
194
|
+
not mean vague: keep every sentence that changes the decision and cut everything that only
|
|
195
|
+
sounds thorough.
|
|
196
|
+
|
|
197
|
+
Before saving, ask: Is the value clear? Can the result be observed? Are decided policies
|
|
198
|
+
separated from guesses? Could clauses fail independently and need splitting? Does every sentence
|
|
199
|
+
earn its place?
|
|
200
|
+
|
|
201
|
+
**Agents always create requirements as draft.** A human must activate them in the web app.
|
|
163
202
|
|
|
164
203
|
### fp propose-change
|
|
165
204
|
|
|
@@ -191,7 +230,7 @@ fp propose-change print_container_qr --project stowzilla \
|
|
|
191
230
|
| `--required` | No | Proposed surfaces (comma-separated; agents cannot remove existing ones) |
|
|
192
231
|
| `--category` | No | Proposed category |
|
|
193
232
|
| `--parent` | No | Proposed parent requirement, by slug |
|
|
194
|
-
| `--slug` | No | Proposed new
|
|
233
|
+
| `--slug` | No | Proposed new key; evidence follows it, but existing `fp:<slug>` markers must be updated |
|
|
195
234
|
| `--reason` | No | Why the change is needed |
|
|
196
235
|
| `--source-type` | No | `fizzy`, `discord`, `github_issue`, `jira`, `notion`, `manual`, `ai_chat` |
|
|
197
236
|
| `--source-url` | No | Link to the card/message/issue that prompted it |
|
|
@@ -204,7 +243,7 @@ Refusals, all printed verbatim:
|
|
|
204
243
|
|
|
205
244
|
| Situation | Status |
|
|
206
245
|
|-----------|--------|
|
|
207
|
-
|
|
|
246
|
+
| Proposed key belongs to another requirement | 422 |
|
|
208
247
|
| Agent tries to remove a required surface | 403 |
|
|
209
248
|
| Proposed values match the current text | 422 |
|
|
210
249
|
| Requirement already has a proposal awaiting review | 409 |
|
|
@@ -26,6 +26,31 @@ Examples:
|
|
|
26
26
|
|
|
27
27
|
When using `fp report --junit`, each pinned surface produces a separate evidence upload.
|
|
28
28
|
|
|
29
|
+
## Test-type pinning
|
|
30
|
+
|
|
31
|
+
A requirement can demand several *kinds* of test (a unit test and an end-to-end test,
|
|
32
|
+
say). Pin a marker's kind with a `:type` suffix, after the surfaces if any:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
fp:<slug>:<type>
|
|
36
|
+
fp:<slug>@<surface>:<type>
|
|
37
|
+
fp:<slug>@<surface1>,<surface2>:<type>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Valid types: `unit`, `integration`, `e2e`, `manual`, `other`.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
- `fp:print_qr` — unspecified type (and the `--surface` default)
|
|
44
|
+
- `fp:print_qr:unit` — filed as a unit test
|
|
45
|
+
- `fp:print_qr@api:e2e` — the `api` surface, filed as an e2e test
|
|
46
|
+
- `fp:print_qr@api,web:e2e` — both surfaces, each filed as an e2e test
|
|
47
|
+
|
|
48
|
+
A marker that pins no type takes the `--test-type` flag's value (if given), otherwise
|
|
49
|
+
`unspecified`. A requirement declares which kinds it needs with
|
|
50
|
+
`fp propose --required-types unit,e2e`; a surface is covered only once every demanded
|
|
51
|
+
kind has evidence. A requirement naming no types inherits the project default, and when
|
|
52
|
+
neither is set any one test satisfies a surface.
|
|
53
|
+
|
|
29
54
|
## Language examples
|
|
30
55
|
|
|
31
56
|
### Ruby/RSpec
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featureparity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09
|
|
11
|
+
date: 2026-10-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: net-http
|
|
@@ -94,10 +94,13 @@ files:
|
|
|
94
94
|
- lib/fp/cli.rb
|
|
95
95
|
- lib/fp/client.rb
|
|
96
96
|
- lib/fp/commands.rb
|
|
97
|
+
- lib/fp/commands/attach.rb
|
|
98
|
+
- lib/fp/commands/attachments.rb
|
|
97
99
|
- lib/fp/commands/base.rb
|
|
98
100
|
- lib/fp/commands/check.rb
|
|
99
101
|
- lib/fp/commands/ci_report.rb
|
|
100
102
|
- lib/fp/commands/config_cmd.rb
|
|
103
|
+
- lib/fp/commands/conversation.rb
|
|
101
104
|
- lib/fp/commands/export.rb
|
|
102
105
|
- lib/fp/commands/help.rb
|
|
103
106
|
- lib/fp/commands/history.rb
|