featureparity 0.0.27 → 0.0.29

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fp/cli.rb +14 -2
  3. data/lib/fp/client.rb +79 -2
  4. data/lib/fp/commands/analyze.rb +17 -0
  5. data/lib/fp/commands/base.rb +16 -0
  6. data/lib/fp/commands/check.rb +38 -4
  7. data/lib/fp/commands/ci_report.rb +2 -2
  8. data/lib/fp/commands/comment.rb +1 -1
  9. data/lib/fp/commands/comments.rb +146 -0
  10. data/lib/fp/commands/coverage.rb +149 -0
  11. data/lib/fp/commands/doctor.rb +228 -0
  12. data/lib/fp/commands/help.rb +41 -1
  13. data/lib/fp/commands/history.rb +16 -1
  14. data/lib/fp/commands/init.rb +181 -117
  15. data/lib/fp/commands/list.rb +58 -4
  16. data/lib/fp/commands/mark.rb +167 -0
  17. data/lib/fp/commands/projects.rb +6 -1
  18. data/lib/fp/commands/propose.rb +435 -53
  19. data/lib/fp/commands/propose_change.rb +13 -7
  20. data/lib/fp/commands/question.rb +137 -0
  21. data/lib/fp/commands/report.rb +40 -4
  22. data/lib/fp/commands/run.rb +147 -0
  23. data/lib/fp/commands/show.rb +41 -3
  24. data/lib/fp/commands/spot_check.rb +458 -0
  25. data/lib/fp/commands/tutorial.rb +95 -0
  26. data/lib/fp/commands/whoami.rb +2 -0
  27. data/lib/fp/commands.rb +8 -0
  28. data/lib/fp/config.rb +12 -0
  29. data/lib/fp/junit.rb +74 -11
  30. data/lib/fp/repo_detector.rb +488 -0
  31. data/lib/fp/requirement_lint.rb +105 -0
  32. data/lib/fp/rspec_json.rb +54 -0
  33. data/lib/fp/tutorial/hexhaven/AGENTS.md +5 -0
  34. data/lib/fp/tutorial/hexhaven/README.md +29 -0
  35. data/lib/fp/tutorial/hexhaven/lib/hexhaven.rb +36 -0
  36. data/lib/fp/tutorial/hexhaven/test/junit_reporter.rb +134 -0
  37. data/lib/fp/tutorial/hexhaven/test/rules_test.rb +36 -0
  38. data/lib/fp/tutorial/hexhaven/test/test_helper.rb +6 -0
  39. data/lib/fp/version.rb +1 -1
  40. data/lib/fp.rb +1 -0
  41. data/skills/fp/SKILL.md +244 -105
  42. data/skills/fp/references/cli.md +304 -3
  43. metadata +19 -2
@@ -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
- help_note 'Makes no API calls, so a repo can be onboarded before anyone has a key.'
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]
@@ -88,8 +107,47 @@ module Fp
88
107
  report(project_slug, root, written, skipped, opts)
89
108
  end
90
109
 
110
+ # The same agent brief `fp init` writes, for another command to put in a repo it
111
+ # creates (`fp tutorial`'s Hexhaven), so the two can never drift apart. Appends to
112
+ # an existing AGENTS.md/CLAUDE.md the way init does. Returns the paths written.
113
+ def self.write_agent_docs!(root, project_slug)
114
+ written = []
115
+ new.send(:write_agent_docs, root, project_slug, { dir: root }, written, [])
116
+ written.map(&:first)
117
+ end
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
+
91
132
  private
92
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
+
93
151
  def write_project_config(root, project_slug, level, opts, written, skipped)
94
152
  path = File.join(root, ProjectConfig::FILENAME)
95
153
  body = ProjectConfig.render(
@@ -187,8 +245,11 @@ module Fp
187
245
  puts ' 2. Add the FP_API_KEY secret to the repo so CI can report evidence and run the gate.'
188
246
  puts " 3. Try the gate locally: fp check#{opts[:surface] ? " --surface #{opts[:surface]}" : ''}"
189
247
  puts
190
- puts 'Until the FP_API_KEY secret is added, the gate skips with an explanation'
191
- puts 'instead of failing — so the first push that onboards the repo stays green.'
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
192
253
  puts
193
254
  puts 'The gate starts at the `regression` level, which only fails on tests that have'
194
255
  puts 'gone red — it will not block you on requirements that were never covered. Raise'
@@ -196,10 +257,100 @@ module Fp
196
257
  end
197
258
  end
198
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
+
199
341
  # The CI gate and its adaptable reporting recipe are separate concerns: the gate
200
342
  # blocks pull requests, while reporting needs a trusted context with FP_API_KEY and
201
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.
202
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
203
354
  <<~YAML
204
355
  # FeatureParity — parity enforcement.
205
356
  #
@@ -341,7 +492,7 @@ module Fp
341
492
  # steps:
342
493
  # - uses: actions/checkout@v4
343
494
  # - name: Run tests (emit JUnit)
344
- # run: echo "replace with your test command emitting junit.xml"
495
+ # run: #{test_command_line}
345
496
  # - name: Report to FeatureParity
346
497
  # if: always() && env.HAVE_FP_KEY == 'true'
347
498
  # env:
@@ -349,7 +500,7 @@ module Fp
349
500
  # run: |
350
501
  # gem install featureparity
351
502
  # args=(
352
- # --junit junit.xml
503
+ # --junit #{report_file}
353
504
  # --surface <surface-key>
354
505
  # --ci-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
355
506
  # )
@@ -368,6 +519,10 @@ module Fp
368
519
  #{AGENTS_MARKER}
369
520
  ## FeatureParity (this project tracks feature parity)
370
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
+
371
526
  This repo is tracked by [FeatureParity](https://featureparity.dev) as project
372
527
  **`#{project_slug}`** (see `.featureparity.yml`). Requirements are shared across every
373
528
  surface the product ships on, and CI enforces that they stay covered — a pull request
@@ -379,6 +534,9 @@ module Fp
379
534
  2. **The requirements stay true.** If none covers what you're building, propose one.
380
535
  If one is wrong or outdated, propose the change — never edit or ignore it.
381
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
+
382
540
  **Start every task by checking the requirements — before you write code, not
383
541
  after.** When you pick up a feature or bug, first inventory the full request and
384
542
  compare it with the active and draft requirements. Find what already covers the work,
@@ -396,66 +554,7 @@ module Fp
396
554
  fp changes --source-url URL # requirement activity prompted by a work item
397
555
  ```
398
556
 
399
- **The requirement catalog is the human-readable product specification, not a box to
400
- tick with one umbrella record.** It records the team's current shared understanding;
401
- it is not a finished decree handed from product to engineering. Refine requirements
402
- through ongoing conversation with customers, domain experts, and builders. The human
403
- product owner decides value and priority; agents should surface gaps, inconsistencies,
404
- alternatives, and useful splits rather than silently making product decisions.
405
-
406
- Before coding, enumerate the independently meaningful promises in the request:
407
- user-visible behaviours, policies, limits, plans, deadlines, state transitions,
408
- permissions, failure modes, and affected surfaces. Then model them:
409
-
410
- - Give each independently testable or independently failable product promise its own
411
- focused requirement. If one acceptance sentence joins behaviours that could pass or
412
- fail separately, split it.
413
- - Use a parent for the broad capability and children for distinct behaviours, policies,
414
- or surface-specific promises. Do not create requirements for implementation details
415
- or refactors that make no product promise.
416
- - Put concrete product decisions — prices, quotas, dates, roles, fallbacks — in the
417
- requirement acceptance criteria. Never leave them only as assumptions in code or a PR.
418
- - Check adjacent requirements and their history. New requirements do not excuse an old
419
- requirement that the work makes inaccurate; propose a change to keep the whole catalog true.
420
- - Before finishing, reconcile the implementation against the catalog: every shipped
421
- behaviour maps to a current requirement, every changed promise is recorded, and every
422
- focused requirement has appropriate marked evidence or an explicit gap. A broad marker
423
- must never appear to prove several acceptance clauses that its test does not exercise.
424
-
425
- **Prevent contradictions before they enter the catalog.** Before proposing a new or changed
426
- promise, compare its intended acceptance criteria with related active and draft requirements
427
- and read their history. Ask whether both promises can be true at the same time for the same
428
- user, state, and conditions. If not, do not add another requirement.
429
-
430
- A failing test bound to an existing requirement, or an implementation that only works by
431
- changing one, is a contradiction signal — not permission to weaken the test or write a
432
- second requirement around it. Identify which promise matches product intent, then use
433
- `fp propose-change` (or `--removal`) with the source and reason to put that decision before
434
- a human. If the intent is unclear, stop and ask rather than making the catalog say both things.
435
-
436
- **Treat existing tests as protected product evidence.** Before editing an existing test,
437
- determine what expectation it already records and whether CI has passing evidence for it.
438
-
439
- - If the test backs passing evidence, do not rewrite it to make the current implementation
440
- or a new requirement fit. Explicitly flag the intended test edit in the work item and pull
441
- request. If its expected behaviour would change, propose the requirement change and wait
442
- for explicit human approval before editing the test; a newly green run must not hide that
443
- the proof itself changed.
444
- - When FeatureParity reports a changed bound test, run `fp changed-tests` (or add `--json`)
445
- and compare the accepted and candidate source revisions with the requirement. Record the
446
- exact candidate through `fp changed-tests assess <id> --candidate-fingerprint <sha256>`
447
- with a plain-language explanation and one classification: `still_proves`,
448
- `no_longer_proves`, or `uncertain`. The first resolves only that fingerprint; the others
449
- remain open for a human product decision. A later fingerprint needs a new assessment.
450
- - A new requirement does not retroactively own or supersede a pre-existing failing test —
451
- whether the requirement is draft or active. Do not change that test, or adopt it as the new
452
- requirement's proof, merely to make the new work pass. Surface the conflict and wait for
453
- explicit human approval to change the existing expectation.
454
-
455
- A changed-test finding is an audit signal alongside the testcase's independent CI outcome.
456
- It is not a failed test and is separate from `suspect`, which means requirement wording
457
- changed and its evidence needs re-verification. Neither permits rewriting a test before
458
- product intent is decided.
557
+ #{shared_guidance('modeling')}
459
558
 
460
559
  If no requirement covers what you're building, propose one or a parent/child tree. Agents
461
560
  always create **drafts**; a human activates them:
@@ -466,56 +565,7 @@ module Fp
466
565
  --source-type fizzy --source-url <card-or-issue-url>
467
566
  ```
468
567
 
469
- **Write a requirement as a product decision someone can approve, not as a design
470
- task.** It should read like a capable teammate wrote it: direct, specific, and free
471
- of boilerplate such as “ensure a seamless experience.”
472
-
473
- - **Title:** one clear product promise in present tense, with a concrete subject and
474
- active verb. Name the user-visible capability, not the work (`“Dispatchers see
475
- current shipment status”`, not `“Implement status polling”`).
476
- - **Why:** one natural sentence naming who needs this, the problem or risk, and the
477
- outcome worth protecting. Do not repeat the title, narrate the request or bug
478
- history, sell the feature, or explain the mechanism.
479
- - **Acceptance:** one independently verifiable rule of observable behaviour that lets a
480
- reviewer decide whether the promise is true. State the trigger or situation, the result, and material
481
- boundaries that qualify that same promise. Given/When/Then may clarify those parts,
482
- but it is not a mandatory template. Split a second result that could fail
483
- independently into its own requirement instead of comma-chaining it here.
484
- - **Example:** supply one concise concrete case that would pass the general rule, and
485
- put it after the rule in Acceptance as `Example: ...`. Use the domain's real terms
486
- and representative data, but ask rather than invent consequential policy. Do not mistake a few examples for the rule
487
- or let marked tests stand for every case; neither replaces the general rule and
488
- continued conversation.
489
- - **Numbers and policy:** include a price, limit, timeout, date, role, or fallback only
490
- when the source made it a real product decision. Never invent a plausible value to
491
- fill a gap. If the decision matters and the source is silent, ask.
492
- - **Implementation detail:** leave out components, data models, algorithms, endpoints,
493
- and UI controls unless that interface or constraint is itself part of the promise.
494
-
495
- Learn from the request, existing requirements and history, and available evidence about
496
- what users actually do. Look for what they attempt, where they abandon a task, what they
497
- use, and what experiments reveal. Treat the first proposed solution as input, not
498
- unquestionable scope. Preserve important uncertainty as a question rather than
499
- laundering a guess into acceptance criteria.
500
-
501
- State the promise itself rather than commentary about it. Avoid openings such as “This
502
- feature…”, “This requirement…”, “The goal is…”, “Users should be able to…”, and “The
503
- system should…”. Use the real actor or product as the subject and an active verb.
504
-
505
- - ✅ **Title:** `Dispatchers see current shipment status`
506
- **Why:** `Dispatchers need to spot delayed shipments before they miss a delivery window.`
507
- **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.`
508
- - ❌ **Title:** `Implement Redis-backed 30-second status polling`
509
- **Why:** `This important feature ensures a seamless, robust experience.`
510
- **Acceptance:** `Use a background worker and refresh every 30 seconds.`
511
- This invents policy, prescribes a design, and never states the product outcome.
512
-
513
- Before proposing, answer three questions against the actual fields: Who is this promise
514
- for? How would the concrete example prove it done? Why does the outcome matter? If any
515
- answer is missing or vague, revise the draft or ask the user. Then read it as a manager deciding scope:
516
- is the value clear, can the result be observed, are product decisions
517
- distinguished from guesses, and does every sentence earn its place? If the draft fails
518
- this review, revise it or ask before writing the record.
568
+ #{shared_guidance('writing')}
519
569
 
520
570
  ### 2. If the requirement is wrong, propose a change — don't silently edit it
521
571
 
@@ -570,6 +620,10 @@ module Fp
570
620
  --work-item <card-or-issue-url>
571
621
  ```
572
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
+
573
627
  Report `present` or `stub` only — **never** claim `passing`/`failing`. Those are CI's
574
628
  to write, via `fp ci-report`, and the gate trusts them precisely because agents can't
575
629
  set them.
@@ -577,6 +631,8 @@ module Fp
577
631
  If you just changed a requirement's wording and re-wrote its tests, re-report the
578
632
  evidence: that's what clears `suspect`.
579
633
 
634
+ #{shared_guidance('media')}
635
+
580
636
  ### 5. Confirm you didn't break the gate
581
637
 
582
638
  ```bash
@@ -585,6 +641,14 @@ module Fp
585
641
 
586
642
  `fp check --json` if you want to parse the violations. Run `fp help` for the full
587
643
  command list.
644
+
645
+ ### Onboarding a repo (first run)
646
+
647
+ #{shared_guidance('onboarding')}
648
+
649
+ ### Troubleshooting
650
+
651
+ #{shared_guidance('troubleshooting')}
588
652
  #{AGENTS_END_MARKER}
589
653
  MARKDOWN
590
654
  end
@@ -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
- def render_gaps(gaps, report, opts)
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