featureparity 0.0.28 → 0.0.30
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 +11 -2
- data/lib/fp/client.rb +31 -1
- data/lib/fp/commands/base.rb +14 -0
- data/lib/fp/commands/check.rb +38 -4
- data/lib/fp/commands/ci_report.rb +35 -5
- data/lib/fp/commands/comment.rb +1 -1
- data/lib/fp/commands/coverage.rb +149 -0
- data/lib/fp/commands/doctor.rb +228 -0
- data/lib/fp/commands/help.rb +13 -1
- data/lib/fp/commands/history.rb +4 -0
- data/lib/fp/commands/init.rb +172 -117
- data/lib/fp/commands/list.rb +58 -4
- data/lib/fp/commands/mark.rb +167 -0
- data/lib/fp/commands/projects.rb +1 -0
- data/lib/fp/commands/propose.rb +401 -52
- data/lib/fp/commands/propose_change.rb +7 -1
- data/lib/fp/commands/question.rb +137 -0
- data/lib/fp/commands/report.rb +23 -3
- data/lib/fp/commands/run.rb +147 -0
- data/lib/fp/commands/show.rb +27 -0
- data/lib/fp/commands.rb +5 -0
- data/lib/fp/junit.rb +60 -7
- data/lib/fp/repo_detector.rb +488 -0
- data/lib/fp/requirement_lint.rb +105 -0
- data/lib/fp/rspec_json.rb +54 -0
- data/lib/fp/version.rb +1 -1
- data/lib/fp.rb +1 -0
- data/skills/fp/SKILL.md +244 -105
- data/skills/fp/references/cli.md +231 -3
- metadata +10 -2
data/lib/fp/commands/init.rb
CHANGED
|
@@ -24,8 +24,10 @@ module Fp
|
|
|
24
24
|
# marker comment on re-run.
|
|
25
25
|
class Init < Base
|
|
26
26
|
help_summary 'Onboard a repo: write .featureparity.yml, a CI gate workflow, and agent docs'
|
|
27
|
-
help_usage 'fp init --project <slug> [--surface X] [--level L] [--dir PATH] [--force]'
|
|
27
|
+
help_usage 'fp init --project <slug> [--surface X] [--level L] [--dir PATH] [--force]',
|
|
28
|
+
'fp init --detect [--dir PATH] [--json]'
|
|
28
29
|
help_flag :project, 'Project slug to record in .featureparity.yml', required: true
|
|
30
|
+
help_flag :detect, 'Suggest a slug, surfaces and test-report commands from the repo; writes nothing'
|
|
29
31
|
help_flag :surface, 'Default surface for this (single-surface) repo'
|
|
30
32
|
help_flag :level, 'Enforcement level to record (usually omit and let the project decide)'
|
|
31
33
|
help_flag :dir, 'Target directory (defaults to the working directory)'
|
|
@@ -34,7 +36,9 @@ module Fp
|
|
|
34
36
|
help_flag :no_agents, 'Skip appending the AGENTS.md section'
|
|
35
37
|
help_example 'fp init --project stowzilla --surface api'
|
|
36
38
|
help_example 'fp init --project stowzilla --no-workflow'
|
|
37
|
-
|
|
39
|
+
help_example 'fp init --detect', 'See what fp makes of this repo before choosing a slug and surfaces'
|
|
40
|
+
help_note "Makes no API calls, so a repo can be onboarded before anyone has a key.\n" \
|
|
41
|
+
'Committing the workflow before the FP_API_KEY secret exists is fine: the gate skips with an explanation.'
|
|
38
42
|
|
|
39
43
|
KNOWN_FLAGS = {
|
|
40
44
|
project: :string,
|
|
@@ -43,7 +47,8 @@ module Fp
|
|
|
43
47
|
dir: :string,
|
|
44
48
|
force: :boolean,
|
|
45
49
|
no_workflow: :boolean,
|
|
46
|
-
no_agents: :boolean
|
|
50
|
+
no_agents: :boolean,
|
|
51
|
+
detect: :boolean
|
|
47
52
|
}.freeze
|
|
48
53
|
|
|
49
54
|
WORKFLOW_PATH = File.join('.github', 'workflows', 'feature-parity.yml')
|
|
@@ -56,6 +61,17 @@ module Fp
|
|
|
56
61
|
AGENTS_MARKER = '<!-- feature-parity:begin -->'
|
|
57
62
|
AGENTS_END_MARKER = '<!-- feature-parity:end -->'
|
|
58
63
|
|
|
64
|
+
# The version line under the section's heading, so `fp doctor` (and a reader) can tell
|
|
65
|
+
# which fp wrote it. Kept out of AGENTS_MARKER so sections written before the stamp
|
|
66
|
+
# are still recognised as ours.
|
|
67
|
+
AGENTS_VERSION_RE = /Written by fp (\d+\.\d+\.\d+[\w.-]*)/
|
|
68
|
+
|
|
69
|
+
# The guidance the skill and this section share lives once, in the skill, between
|
|
70
|
+
# `<!-- fp-shared:begin NAME -->` / `<!-- fp-shared:end NAME -->`, and is copied in
|
|
71
|
+
# here (Fizzy #1786). Two hand-kept copies had drifted, and an agent with both the
|
|
72
|
+
# skill and AGENTS.md read the same pages twice.
|
|
73
|
+
SKILL_PATH = File.expand_path('../../../skills/fp/SKILL.md', __dir__)
|
|
74
|
+
|
|
59
75
|
def run(args)
|
|
60
76
|
opts, = parse_flags(args, KNOWN_FLAGS)
|
|
61
77
|
|
|
@@ -65,6 +81,8 @@ module Fp
|
|
|
65
81
|
exit 1
|
|
66
82
|
end
|
|
67
83
|
|
|
84
|
+
return print_detection(RepoDetector.new(root).detect) if opts[:detect]
|
|
85
|
+
|
|
68
86
|
existing = ProjectConfig.discover(root)
|
|
69
87
|
project_slug = opts[:project] || existing.project
|
|
70
88
|
unless project_slug
|
|
@@ -80,6 +98,7 @@ module Fp
|
|
|
80
98
|
|
|
81
99
|
written = []
|
|
82
100
|
skipped = []
|
|
101
|
+
@detection = RepoDetector.new(root).detect
|
|
83
102
|
|
|
84
103
|
write_project_config(root, project_slug, level, opts, written, skipped)
|
|
85
104
|
write_workflow(root, opts, written, skipped) unless opts[:no_workflow]
|
|
@@ -97,8 +116,38 @@ module Fp
|
|
|
97
116
|
written.map(&:first)
|
|
98
117
|
end
|
|
99
118
|
|
|
119
|
+
# One shared block from the skill, markers included. A gem missing its skill (it
|
|
120
|
+
# ships in spec.files, so only a broken install) still gets a pointer, not a crash.
|
|
121
|
+
def self.shared_guidance(name)
|
|
122
|
+
@shared_guidance ||= {}
|
|
123
|
+
@shared_guidance[name] ||= begin
|
|
124
|
+
text = File.read(SKILL_PATH)
|
|
125
|
+
pattern = /<!-- fp-shared:begin #{Regexp.escape(name)} -->.*?<!-- fp-shared:end #{Regexp.escape(name)} -->/m
|
|
126
|
+
text[pattern] || "See the `fp` agent skill (#{name})."
|
|
127
|
+
rescue SystemCallError
|
|
128
|
+
"See the `fp` agent skill bundled with the featureparity gem (#{name})."
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
100
132
|
private
|
|
101
133
|
|
|
134
|
+
def shared_guidance(name)
|
|
135
|
+
self.class.shared_guidance(name)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# `fp init --detect`: what the repo looks like, nothing written.
|
|
139
|
+
def print_detection(detection)
|
|
140
|
+
output.success(detection) do
|
|
141
|
+
RepoDetector.print_summary(detection)
|
|
142
|
+
puts
|
|
143
|
+
puts 'Nothing was written. To onboard with these suggestions:'
|
|
144
|
+
keys = detection['surfaces'].map { |s| s['key'] }
|
|
145
|
+
surfaces = keys.empty? ? '<keys>' : keys.join(',')
|
|
146
|
+
puts " fp projects create #{detection['slug']} --name \"...\" --surfaces #{surfaces} # if it doesn't exist yet"
|
|
147
|
+
puts " fp init --project #{detection['slug']}#{keys.length == 1 ? " --surface #{keys.first}" : ''}"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
102
151
|
def write_project_config(root, project_slug, level, opts, written, skipped)
|
|
103
152
|
path = File.join(root, ProjectConfig::FILENAME)
|
|
104
153
|
body = ProjectConfig.render(
|
|
@@ -196,8 +245,11 @@ module Fp
|
|
|
196
245
|
puts ' 2. Add the FP_API_KEY secret to the repo so CI can report evidence and run the gate.'
|
|
197
246
|
puts " 3. Try the gate locally: fp check#{opts[:surface] ? " --surface #{opts[:surface]}" : ''}"
|
|
198
247
|
puts
|
|
199
|
-
puts '
|
|
200
|
-
puts 'instead of failing
|
|
248
|
+
puts 'Commit these files now, even before the FP_API_KEY secret exists: until it is'
|
|
249
|
+
puts 'added, the gate skips with an explanation instead of failing, so the first push'
|
|
250
|
+
puts 'that onboards the repo stays green.'
|
|
251
|
+
puts
|
|
252
|
+
print_test_guidance
|
|
201
253
|
puts
|
|
202
254
|
puts 'The gate starts at the `regression` level, which only fails on tests that have'
|
|
203
255
|
puts 'gone red — it will not block you on requirements that were never covered. Raise'
|
|
@@ -205,10 +257,100 @@ module Fp
|
|
|
205
257
|
end
|
|
206
258
|
end
|
|
207
259
|
|
|
260
|
+
# The detected test-report command(s) and the database note, so the first report
|
|
261
|
+
# doesn't start with working out how to get a report out of the suite.
|
|
262
|
+
def print_test_guidance
|
|
263
|
+
detection = @detection || {}
|
|
264
|
+
runners = detection['test_runners'] || []
|
|
265
|
+
if runners.empty?
|
|
266
|
+
puts 'No test runner recognised. Any runner that writes JUnit XML works: fp report --junit <file>.'
|
|
267
|
+
else
|
|
268
|
+
puts 'To report evidence from your tests:'
|
|
269
|
+
runners.each do |r|
|
|
270
|
+
puts " #{r['name']}: #{r['fp_run'] || r['report_command']}"
|
|
271
|
+
puts " or write the report yourself: #{r['report_command']}" if r['fp_run']
|
|
272
|
+
puts " needs #{r['dependency']}" if r['dependency']
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
note = RepoDetector.database_note(detection['database'])
|
|
276
|
+
if note
|
|
277
|
+
puts
|
|
278
|
+
puts note
|
|
279
|
+
puts "A devcontainer is at #{detection['devcontainer']} and may provide it locally." if detection['devcontainer']
|
|
280
|
+
end
|
|
281
|
+
puts
|
|
282
|
+
puts 'Run `fp doctor` any time to check fp, its config and this repo.'
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# A commented-out service for the report job when the tests need a database server,
|
|
286
|
+
# since the recipe can't run them without one.
|
|
287
|
+
DATABASE_SERVICES = {
|
|
288
|
+
'postgresql' => <<~YAML,
|
|
289
|
+
# services:
|
|
290
|
+
# postgres:
|
|
291
|
+
# image: postgres:16
|
|
292
|
+
# env:
|
|
293
|
+
# POSTGRES_PASSWORD: postgres
|
|
294
|
+
# ports: ['5432:5432']
|
|
295
|
+
# options: >-
|
|
296
|
+
# --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
|
297
|
+
YAML
|
|
298
|
+
'mysql' => <<~YAML,
|
|
299
|
+
# services:
|
|
300
|
+
# mysql:
|
|
301
|
+
# image: mysql:8
|
|
302
|
+
# env:
|
|
303
|
+
# MYSQL_ROOT_PASSWORD: root
|
|
304
|
+
# ports: ['3306:3306']
|
|
305
|
+
# options: >-
|
|
306
|
+
# --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
|
307
|
+
YAML
|
|
308
|
+
'mongodb' => <<~YAML
|
|
309
|
+
# services:
|
|
310
|
+
# mongodb:
|
|
311
|
+
# image: mongo:7
|
|
312
|
+
# ports: ['27017:27017']
|
|
313
|
+
YAML
|
|
314
|
+
}.freeze
|
|
315
|
+
|
|
316
|
+
def database_lines
|
|
317
|
+
db = (@detection || {})['database']
|
|
318
|
+
return '' unless db && db['needed']
|
|
319
|
+
|
|
320
|
+
kinds = db['kinds'] & RepoDetector::SERVER_DATABASES
|
|
321
|
+
services = kinds.filter_map { |k| DATABASE_SERVICES[k] }.first.to_s
|
|
322
|
+
indent = ' '
|
|
323
|
+
lines = ["# # TODO: the tests need #{kinds.join('/')} (#{db['evidence'].first(3).join('; ')}).",
|
|
324
|
+
'# # This job has no database service: uncomment and adapt the one below, and point',
|
|
325
|
+
'# # the app at it (e.g. DATABASE_URL), or the suite cannot run here.']
|
|
326
|
+
(lines + services.lines.map(&:chomp)).map { |l| "#{indent}#{l}\n" }.join
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def test_command_line
|
|
330
|
+
runner = ((@detection || {})['test_runners'] || []).first
|
|
331
|
+
return 'echo "replace with your test command emitting junit.xml"' unless runner
|
|
332
|
+
|
|
333
|
+
runner['report_command']
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def report_file
|
|
337
|
+
runner = ((@detection || {})['test_runners'] || []).first
|
|
338
|
+
runner && !runner['report_file'].to_s.end_with?('test-results') ? runner['report_file'] : 'junit.xml'
|
|
339
|
+
end
|
|
340
|
+
|
|
208
341
|
# The CI gate and its adaptable reporting recipe are separate concerns: the gate
|
|
209
342
|
# blocks pull requests, while reporting needs a trusted context with FP_API_KEY and
|
|
210
343
|
# should cover both pull requests and default-branch pushes.
|
|
344
|
+
#
|
|
345
|
+
# The recipe's test step uses the report command detected for this repo, and when the
|
|
346
|
+
# tests need a database server a commented-out service goes in ahead of its steps.
|
|
211
347
|
def workflow_body
|
|
348
|
+
body = workflow_template
|
|
349
|
+
db = database_lines
|
|
350
|
+
db.empty? ? body : body.sub(" # steps:\n", "#{db} # steps:\n")
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def workflow_template
|
|
212
354
|
<<~YAML
|
|
213
355
|
# FeatureParity — parity enforcement.
|
|
214
356
|
#
|
|
@@ -350,7 +492,7 @@ module Fp
|
|
|
350
492
|
# steps:
|
|
351
493
|
# - uses: actions/checkout@v4
|
|
352
494
|
# - name: Run tests (emit JUnit)
|
|
353
|
-
# run:
|
|
495
|
+
# run: #{test_command_line}
|
|
354
496
|
# - name: Report to FeatureParity
|
|
355
497
|
# if: always() && env.HAVE_FP_KEY == 'true'
|
|
356
498
|
# env:
|
|
@@ -358,7 +500,7 @@ module Fp
|
|
|
358
500
|
# run: |
|
|
359
501
|
# gem install featureparity
|
|
360
502
|
# args=(
|
|
361
|
-
# --junit
|
|
503
|
+
# --junit #{report_file}
|
|
362
504
|
# --surface <surface-key>
|
|
363
505
|
# --ci-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
364
506
|
# )
|
|
@@ -377,6 +519,10 @@ module Fp
|
|
|
377
519
|
#{AGENTS_MARKER}
|
|
378
520
|
## FeatureParity (this project tracks feature parity)
|
|
379
521
|
|
|
522
|
+
_Written by fp #{Fp::VERSION} (`fp init`). `fp init --force` refreshes it, and `fp doctor`
|
|
523
|
+
says when the installed fp is newer. The blocks between `fp-shared` comments are the
|
|
524
|
+
`fp` agent skill's own text: if you have the skill loaded, you've read them already._
|
|
525
|
+
|
|
380
526
|
This repo is tracked by [FeatureParity](https://featureparity.dev) as project
|
|
381
527
|
**`#{project_slug}`** (see `.featureparity.yml`). Requirements are shared across every
|
|
382
528
|
surface the product ships on, and CI enforces that they stay covered — a pull request
|
|
@@ -388,6 +534,9 @@ module Fp
|
|
|
388
534
|
2. **The requirements stay true.** If none covers what you're building, propose one.
|
|
389
535
|
If one is wrong or outdated, propose the change — never edit or ignore it.
|
|
390
536
|
|
|
537
|
+
**First run in this repo?** The loop below assumes the catalog already describes the
|
|
538
|
+
product. If it doesn't yet, follow *Onboarding a repo* near the end of this section first.
|
|
539
|
+
|
|
391
540
|
**Start every task by checking the requirements — before you write code, not
|
|
392
541
|
after.** When you pick up a feature or bug, first inventory the full request and
|
|
393
542
|
compare it with the active and draft requirements. Find what already covers the work,
|
|
@@ -405,66 +554,7 @@ module Fp
|
|
|
405
554
|
fp changes --source-url URL # requirement activity prompted by a work item
|
|
406
555
|
```
|
|
407
556
|
|
|
408
|
-
|
|
409
|
-
tick with one umbrella record.** It records the team's current shared understanding;
|
|
410
|
-
it is not a finished decree handed from product to engineering. Refine requirements
|
|
411
|
-
through ongoing conversation with customers, domain experts, and builders. The human
|
|
412
|
-
product owner decides value and priority; agents should surface gaps, inconsistencies,
|
|
413
|
-
alternatives, and useful splits rather than silently making product decisions.
|
|
414
|
-
|
|
415
|
-
Before coding, enumerate the independently meaningful promises in the request:
|
|
416
|
-
user-visible behaviours, policies, limits, plans, deadlines, state transitions,
|
|
417
|
-
permissions, failure modes, and affected surfaces. Then model them:
|
|
418
|
-
|
|
419
|
-
- Give each independently testable or independently failable product promise its own
|
|
420
|
-
focused requirement. If one acceptance sentence joins behaviours that could pass or
|
|
421
|
-
fail separately, split it.
|
|
422
|
-
- Use a parent for the broad capability and children for distinct behaviours, policies,
|
|
423
|
-
or surface-specific promises. Do not create requirements for implementation details
|
|
424
|
-
or refactors that make no product promise.
|
|
425
|
-
- Put concrete product decisions — prices, quotas, dates, roles, fallbacks — in the
|
|
426
|
-
requirement acceptance criteria. Never leave them only as assumptions in code or a PR.
|
|
427
|
-
- Check adjacent requirements and their history. New requirements do not excuse an old
|
|
428
|
-
requirement that the work makes inaccurate; propose a change to keep the whole catalog true.
|
|
429
|
-
- Before finishing, reconcile the implementation against the catalog: every shipped
|
|
430
|
-
behaviour maps to a current requirement, every changed promise is recorded, and every
|
|
431
|
-
focused requirement has appropriate marked evidence or an explicit gap. A broad marker
|
|
432
|
-
must never appear to prove several acceptance clauses that its test does not exercise.
|
|
433
|
-
|
|
434
|
-
**Prevent contradictions before they enter the catalog.** Before proposing a new or changed
|
|
435
|
-
promise, compare its intended acceptance criteria with related active and draft requirements
|
|
436
|
-
and read their history. Ask whether both promises can be true at the same time for the same
|
|
437
|
-
user, state, and conditions. If not, do not add another requirement.
|
|
438
|
-
|
|
439
|
-
A failing test bound to an existing requirement, or an implementation that only works by
|
|
440
|
-
changing one, is a contradiction signal — not permission to weaken the test or write a
|
|
441
|
-
second requirement around it. Identify which promise matches product intent, then use
|
|
442
|
-
`fp propose-change` (or `--removal`) with the source and reason to put that decision before
|
|
443
|
-
a human. If the intent is unclear, stop and ask rather than making the catalog say both things.
|
|
444
|
-
|
|
445
|
-
**Treat existing tests as protected product evidence.** Before editing an existing test,
|
|
446
|
-
determine what expectation it already records and whether CI has passing evidence for it.
|
|
447
|
-
|
|
448
|
-
- If the test backs passing evidence, do not rewrite it to make the current implementation
|
|
449
|
-
or a new requirement fit. Explicitly flag the intended test edit in the work item and pull
|
|
450
|
-
request. If its expected behaviour would change, propose the requirement change and wait
|
|
451
|
-
for explicit human approval before editing the test; a newly green run must not hide that
|
|
452
|
-
the proof itself changed.
|
|
453
|
-
- When FeatureParity reports a changed bound test, run `fp changed-tests` (or add `--json`)
|
|
454
|
-
and compare the accepted and candidate source revisions with the requirement. Record the
|
|
455
|
-
exact candidate through `fp changed-tests assess <id> --candidate-fingerprint <sha256>`
|
|
456
|
-
with a plain-language explanation and one classification: `still_proves`,
|
|
457
|
-
`no_longer_proves`, or `uncertain`. The first resolves only that fingerprint; the others
|
|
458
|
-
remain open for a human product decision. A later fingerprint needs a new assessment.
|
|
459
|
-
- A new requirement does not retroactively own or supersede a pre-existing failing test —
|
|
460
|
-
whether the requirement is draft or active. Do not change that test, or adopt it as the new
|
|
461
|
-
requirement's proof, merely to make the new work pass. Surface the conflict and wait for
|
|
462
|
-
explicit human approval to change the existing expectation.
|
|
463
|
-
|
|
464
|
-
A changed-test finding is an audit signal alongside the testcase's independent CI outcome.
|
|
465
|
-
It is not a failed test and is separate from `suspect`, which means requirement wording
|
|
466
|
-
changed and its evidence needs re-verification. Neither permits rewriting a test before
|
|
467
|
-
product intent is decided.
|
|
557
|
+
#{shared_guidance('modeling')}
|
|
468
558
|
|
|
469
559
|
If no requirement covers what you're building, propose one or a parent/child tree. Agents
|
|
470
560
|
always create **drafts**; a human activates them:
|
|
@@ -475,56 +565,7 @@ module Fp
|
|
|
475
565
|
--source-type fizzy --source-url <card-or-issue-url>
|
|
476
566
|
```
|
|
477
567
|
|
|
478
|
-
|
|
479
|
-
task.** It should read like a capable teammate wrote it: direct, specific, and free
|
|
480
|
-
of boilerplate such as “ensure a seamless experience.”
|
|
481
|
-
|
|
482
|
-
- **Title:** one clear product promise in present tense, with a concrete subject and
|
|
483
|
-
active verb. Name the user-visible capability, not the work (`“Dispatchers see
|
|
484
|
-
current shipment status”`, not `“Implement status polling”`).
|
|
485
|
-
- **Why:** one natural sentence naming who needs this, the problem or risk, and the
|
|
486
|
-
outcome worth protecting. Do not repeat the title, narrate the request or bug
|
|
487
|
-
history, sell the feature, or explain the mechanism.
|
|
488
|
-
- **Acceptance:** one independently verifiable rule of observable behaviour that lets a
|
|
489
|
-
reviewer decide whether the promise is true. State the trigger or situation, the result, and material
|
|
490
|
-
boundaries that qualify that same promise. Given/When/Then may clarify those parts,
|
|
491
|
-
but it is not a mandatory template. Split a second result that could fail
|
|
492
|
-
independently into its own requirement instead of comma-chaining it here.
|
|
493
|
-
- **Example:** supply one concise concrete case that would pass the general rule, and
|
|
494
|
-
put it after the rule in Acceptance as `Example: ...`. Use the domain's real terms
|
|
495
|
-
and representative data, but ask rather than invent consequential policy. Do not mistake a few examples for the rule
|
|
496
|
-
or let marked tests stand for every case; neither replaces the general rule and
|
|
497
|
-
continued conversation.
|
|
498
|
-
- **Numbers and policy:** include a price, limit, timeout, date, role, or fallback only
|
|
499
|
-
when the source made it a real product decision. Never invent a plausible value to
|
|
500
|
-
fill a gap. If the decision matters and the source is silent, ask.
|
|
501
|
-
- **Implementation detail:** leave out components, data models, algorithms, endpoints,
|
|
502
|
-
and UI controls unless that interface or constraint is itself part of the promise.
|
|
503
|
-
|
|
504
|
-
Learn from the request, existing requirements and history, and available evidence about
|
|
505
|
-
what users actually do. Look for what they attempt, where they abandon a task, what they
|
|
506
|
-
use, and what experiments reveal. Treat the first proposed solution as input, not
|
|
507
|
-
unquestionable scope. Preserve important uncertainty as a question rather than
|
|
508
|
-
laundering a guess into acceptance criteria.
|
|
509
|
-
|
|
510
|
-
State the promise itself rather than commentary about it. Avoid openings such as “This
|
|
511
|
-
feature…”, “This requirement…”, “The goal is…”, “Users should be able to…”, and “The
|
|
512
|
-
system should…”. Use the real actor or product as the subject and an active verb.
|
|
513
|
-
|
|
514
|
-
- ✅ **Title:** `Dispatchers see current shipment status`
|
|
515
|
-
**Why:** `Dispatchers need to spot delayed shipments before they miss a delivery window.`
|
|
516
|
-
**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.`
|
|
517
|
-
- ❌ **Title:** `Implement Redis-backed 30-second status polling`
|
|
518
|
-
**Why:** `This important feature ensures a seamless, robust experience.`
|
|
519
|
-
**Acceptance:** `Use a background worker and refresh every 30 seconds.`
|
|
520
|
-
This invents policy, prescribes a design, and never states the product outcome.
|
|
521
|
-
|
|
522
|
-
Before proposing, answer three questions against the actual fields: Who is this promise
|
|
523
|
-
for? How would the concrete example prove it done? Why does the outcome matter? If any
|
|
524
|
-
answer is missing or vague, revise the draft or ask the user. Then read it as a manager deciding scope:
|
|
525
|
-
is the value clear, can the result be observed, are product decisions
|
|
526
|
-
distinguished from guesses, and does every sentence earn its place? If the draft fails
|
|
527
|
-
this review, revise it or ask before writing the record.
|
|
568
|
+
#{shared_guidance('writing')}
|
|
528
569
|
|
|
529
570
|
### 2. If the requirement is wrong, propose a change — don't silently edit it
|
|
530
571
|
|
|
@@ -579,6 +620,10 @@ module Fp
|
|
|
579
620
|
--work-item <card-or-issue-url>
|
|
580
621
|
```
|
|
581
622
|
|
|
623
|
+
`--junit` also takes RSpec's own `--format json` report. To run the suite and report in
|
|
624
|
+
one step with no formatter added to the project (rspec, pytest, vitest, playwright):
|
|
625
|
+
`fp run --surface <surface> --repo <org/repo> -- <test command>`.
|
|
626
|
+
|
|
582
627
|
Report `present` or `stub` only — **never** claim `passing`/`failing`. Those are CI's
|
|
583
628
|
to write, via `fp ci-report`, and the gate trusts them precisely because agents can't
|
|
584
629
|
set them.
|
|
@@ -586,6 +631,8 @@ module Fp
|
|
|
586
631
|
If you just changed a requirement's wording and re-wrote its tests, re-report the
|
|
587
632
|
evidence: that's what clears `suspect`.
|
|
588
633
|
|
|
634
|
+
#{shared_guidance('media')}
|
|
635
|
+
|
|
589
636
|
### 5. Confirm you didn't break the gate
|
|
590
637
|
|
|
591
638
|
```bash
|
|
@@ -594,6 +641,14 @@ module Fp
|
|
|
594
641
|
|
|
595
642
|
`fp check --json` if you want to parse the violations. Run `fp help` for the full
|
|
596
643
|
command list.
|
|
644
|
+
|
|
645
|
+
### Onboarding a repo (first run)
|
|
646
|
+
|
|
647
|
+
#{shared_guidance('onboarding')}
|
|
648
|
+
|
|
649
|
+
### Troubleshooting
|
|
650
|
+
|
|
651
|
+
#{shared_guidance('troubleshooting')}
|
|
597
652
|
#{AGENTS_END_MARKER}
|
|
598
653
|
MARKDOWN
|
|
599
654
|
end
|
data/lib/fp/commands/list.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Fp
|
|
|
16
16
|
help_example 'fp list --project stowzilla'
|
|
17
17
|
help_example 'fp list --project stowzilla --status draft', 'Drafts are hidden by default'
|
|
18
18
|
help_example 'fp list --project stowzilla --gaps --surface customer_android'
|
|
19
|
+
help_example 'fp list --status draft --gaps', 'Drafts lacking evidence on a required surface'
|
|
19
20
|
|
|
20
21
|
KNOWN_FLAGS = {
|
|
21
22
|
project: :string,
|
|
@@ -36,7 +37,9 @@ module Fp
|
|
|
36
37
|
# rather than passing --surface on every call.
|
|
37
38
|
opts[:surface] ||= project_config.surface
|
|
38
39
|
|
|
39
|
-
if opts[:gaps]
|
|
40
|
+
if opts[:gaps] && opts[:status] == 'draft'
|
|
41
|
+
list_draft_gaps(project_id, opts)
|
|
42
|
+
elsif opts[:gaps]
|
|
40
43
|
list_gaps(project_id, opts)
|
|
41
44
|
else
|
|
42
45
|
list_requirements(project_id, opts)
|
|
@@ -191,7 +194,44 @@ module Fp
|
|
|
191
194
|
render_gaps(gaps, report, opts)
|
|
192
195
|
end
|
|
193
196
|
|
|
194
|
-
|
|
197
|
+
# Draft gaps: `--status draft --gaps`. The gate doesn't measure never-approved
|
|
198
|
+
# drafts, so the plain gaps read above can't see them and this used to print "No
|
|
199
|
+
# gaps found" on a project whose every requirement was an untested draft (Fizzy
|
|
200
|
+
# #1786). The parity report's `drafts` section resolves each draft's cells from
|
|
201
|
+
# evidence the way the grid would; a gap here is the same thing it is above — a
|
|
202
|
+
# cell in a state that violates at `covered` (missing, stub, failing) — read from
|
|
203
|
+
# the report's own violating_states rather than decided here. Informational: drafts
|
|
204
|
+
# never fail a gate.
|
|
205
|
+
def list_draft_gaps(project_id, opts)
|
|
206
|
+
result = client.get_parity_with_drafts(project_id, level: 'covered', surface: opts[:surface])
|
|
207
|
+
|
|
208
|
+
unless result[:ok]
|
|
209
|
+
output.error(result[:error], status: result[:status])
|
|
210
|
+
exit 1
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
report = result[:data]['parity'] || {}
|
|
214
|
+
unless report.key?('drafts')
|
|
215
|
+
output.error('This FeatureParity API does not report draft coverage yet (it predates ' \
|
|
216
|
+
'`include_drafts`), so draft gaps cannot be listed.')
|
|
217
|
+
exit 1
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
gap_states = report['violating_states'] || []
|
|
221
|
+
gaps = (report['drafts'] || []).filter_map do |row|
|
|
222
|
+
uncovered = Array(row['cells']).select { |c| gap_states.include?(c['state']) }
|
|
223
|
+
next if uncovered.empty?
|
|
224
|
+
|
|
225
|
+
{ 'slug' => row['slug'], 'title' => row['title'], 'category' => row['category'],
|
|
226
|
+
'under_review' => false,
|
|
227
|
+
'surfaces' => uncovered.map { |c| { 'surface' => c['surface'], 'state' => c['state'] } } }
|
|
228
|
+
end
|
|
229
|
+
gaps = gaps.select { |gap| gap['category'] == opts[:category] } if opts[:category]
|
|
230
|
+
|
|
231
|
+
render_gaps(gaps, report, opts, drafts: true)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def render_gaps(gaps, report, opts, drafts: false)
|
|
195
235
|
payload = {
|
|
196
236
|
gaps: gaps,
|
|
197
237
|
project: opts[:project],
|
|
@@ -199,9 +239,18 @@ module Fp
|
|
|
199
239
|
category: opts[:category],
|
|
200
240
|
summary: report['summary']
|
|
201
241
|
}
|
|
242
|
+
payload[:status] = 'draft' if drafts
|
|
202
243
|
|
|
203
244
|
output.success(payload) do
|
|
204
|
-
if gaps.empty?
|
|
245
|
+
if gaps.empty? && drafts
|
|
246
|
+
filter_desc = []
|
|
247
|
+
filter_desc << "category: #{opts[:category]}" if opts[:category]
|
|
248
|
+
filter_desc << "surface: #{opts[:surface]}" if opts[:surface]
|
|
249
|
+
msg = (report['drafts'] || []).empty? ? 'No draft requirements' : 'No draft gaps found'
|
|
250
|
+
msg += " (#{filter_desc.join(', ')})" unless filter_desc.empty?
|
|
251
|
+
puts "#{msg}."
|
|
252
|
+
next
|
|
253
|
+
elsif gaps.empty?
|
|
205
254
|
filter_desc = []
|
|
206
255
|
filter_desc << "category: #{opts[:category]}" if opts[:category]
|
|
207
256
|
filter_desc << "surface: #{opts[:surface]}" if opts[:surface]
|
|
@@ -217,7 +266,7 @@ module Fp
|
|
|
217
266
|
next
|
|
218
267
|
end
|
|
219
268
|
|
|
220
|
-
desc = "Gaps for project '#{opts[:project]}'"
|
|
269
|
+
desc = "#{drafts ? 'Draft gaps' : 'Gaps'} for project '#{opts[:project]}'"
|
|
221
270
|
desc += " (surface: #{opts[:surface]})" if opts[:surface]
|
|
222
271
|
desc += " (category: #{opts[:category]})" if opts[:category]
|
|
223
272
|
puts "#{desc}:"
|
|
@@ -234,6 +283,11 @@ module Fp
|
|
|
234
283
|
puts
|
|
235
284
|
puts "#{Parity::UNDER_REVIEW} #{Parity::UNDER_REVIEW_LEGEND} — check `fp history <slug>` before writing a test against wording that may be about to move."
|
|
236
285
|
end
|
|
286
|
+
|
|
287
|
+
if drafts
|
|
288
|
+
puts
|
|
289
|
+
puts 'Drafts are reported, never gated. See every draft × surface with: fp coverage'
|
|
290
|
+
end
|
|
237
291
|
end
|
|
238
292
|
end
|
|
239
293
|
|