featureparity 0.0.21 → 0.0.23

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.
@@ -36,6 +36,21 @@ module Fp
36
36
  # one. Prefer setting the project's level to `regression` over living on --warn-only
37
37
  # forever, since a warn-only gate is one nobody reads.
38
38
  class Check < Base
39
+ help_summary 'Enforce parity; exits non-zero when the project violates its level'
40
+ help_usage 'fp check [--project <slug>] [--surface X] [--level L] [--junit FILE]... [--warn-only]'
41
+ help_flag :project, 'Project slug (falls back to .featureparity.yml)'
42
+ help_flag :surface, 'Gate a single surface'
43
+ help_flag :test_type, 'Gate a single test type'
44
+ help_flag :level, 'Override the enforcement level for this run (off/regression/covered/strict)'
45
+ help_flag :junit, "Gate the branch on this run's results, overlaid on the grid (repeatable, one per surface)"
46
+ help_flag :base_dir, "Where the report's relative test-file paths resolve from"
47
+ help_flag :warn_only, 'Report violations but still exit 0'
48
+ help_example 'fp check', 'Gate on recorded state (project from .featureparity.yml)'
49
+ help_example 'fp check --surface api --junit junit.xml', "Gate a PR branch on its own results"
50
+ help_example 'fp check --warn-only --json'
51
+ help_note "Exit 0 = passed, 1 = violations (or request failed). The level lives on the project\n" \
52
+ 'server-side; --level only overrides this one run.'
53
+
39
54
  KNOWN_FLAGS = {
40
55
  project: :string,
41
56
  surface: :string,
@@ -182,11 +197,12 @@ module Fp
182
197
  end
183
198
 
184
199
  print_state_counts(summary)
200
+ print_scope_notice(report)
185
201
  print_violations(violations)
186
202
  print_orphans(report)
187
203
  print_pending_reviews(report, project_slug)
188
204
  print_unscoped_warning(summary, project_slug)
189
- print_verdict(passed, violations, opts)
205
+ print_verdict(passed, violations, opts, report)
190
206
  end
191
207
  end
192
208
 
@@ -198,6 +214,24 @@ module Fp
198
214
  puts
199
215
  end
200
216
 
217
+ # A surface-scoped run (--surface, or a single `surface:` in .featureparity.yml)
218
+ # only measures the one surface it was given. That's correct for a per-surface CI
219
+ # job, but it means the verdict covers a slice of the project — so a bare "Gate
220
+ # passed" would read as "the whole project is in parity" when other required
221
+ # surfaces were never looked at (Fizzy #1490). Name them up front, and the verdict
222
+ # line qualifies itself to match.
223
+ def print_scope_notice(report)
224
+ return unless report['scoped']
225
+
226
+ excluded = report['scoped_out_surfaces'] || []
227
+ return if excluded.empty?
228
+
229
+ puts "Scope: this check only evaluated the '#{report['surface']}' surface."
230
+ puts " #{excluded.size} other required surface(s) were NOT checked: #{excluded.join(', ')}."
231
+ puts ' Run fp check per surface (or without --surface) to cover the whole project.'
232
+ puts
233
+ end
234
+
201
235
  # Requirements a proposed change has knocked back to draft. Informational, never a
202
236
  # violation — a human's review latency is not something to fail an agent's build
203
237
  # over. But they stay in the grid (proposing a change must not be a way out of the
@@ -302,9 +336,13 @@ module Fp
302
336
  puts
303
337
  end
304
338
 
305
- def print_verdict(passed, violations, opts)
339
+ def print_verdict(passed, violations, opts, report = {})
340
+ # A scoped pass is not a project pass: say which surface it passed for, so nobody
341
+ # reads "Gate passed" as "the whole project is covered" (Fizzy #1490).
342
+ scope = report['scoped'] ? " for surface '#{report['surface']}' (other required surfaces not checked)" : ''
343
+
306
344
  if passed
307
- puts 'Gate passed.'
345
+ puts "Gate passed#{scope}."
308
346
  elsif opts[:warn_only]
309
347
  puts "Gate FAILED with #{violations.size} violation(s) — exiting 0 because --warn-only was given."
310
348
  else
@@ -50,6 +50,23 @@ module Fp
50
50
  # the default branch to record the real `passing`, superseding any pr_passing a prior
51
51
  # PR run left behind.
52
52
  class CiReport < Base
53
+ help_summary 'CI only: ingest real pass/fail results (writes passing/failing evidence)'
54
+ help_usage 'fp ci-report --junit <file>... --project <slug> --surface <surface> --ci-url URL [--pr URL]'
55
+ help_flag :project, 'Project slug (falls back to .featureparity.yml)'
56
+ help_flag :surface, 'Surface the suite covers', required: true
57
+ help_flag :test_type, 'Default test kind for markers that pin none'
58
+ help_flag :ci_url, 'CI run URL to record on the evidence'
59
+ help_flag :pr, "Mark this as a PR's CI: passing tests recorded as pr_passing, no merged evidence demoted"
60
+ help_flag :junit, 'JUnit XML report (repeatable to combine a surface split across shards)'
61
+ help_flag :base_dir, "Where the report's relative test-file paths resolve from (e.g. frontend)"
62
+ help_flag :repo, 'Repository the tests live in (org/repo)'
63
+ help_flag :sha, 'Checkout commit SHA'
64
+ help_flag :allow_empty, 'Do not error when a report resolves no fp:<slug> markers'
65
+ help_example 'fp ci-report --junit junit.xml --project stowzilla --surface api --ci-url "$CI_RUN_URL"'
66
+ help_example 'fp ci-report --junit web.xml --surface web --base-dir frontend --pr "$PR_URL"'
67
+ help_note "This is CI's command, not an agent's: only it writes passing/failing. Run it on both\n" \
68
+ 'green and red builds (if: always()) so failures show on the matrix.'
69
+
53
70
  KNOWN_FLAGS = {
54
71
  project: :string,
55
72
  surface: :string,
@@ -19,6 +19,16 @@ module Fp
19
19
 
20
20
  VALID_SOURCE_TYPES = %w[fizzy discord github_issue jira notion manual ai_chat].freeze
21
21
 
22
+ help_summary "Comment on a requirement's open change proposal"
23
+ help_usage 'fp comment <slug> --body "..." --project <slug>'
24
+ help_flag :project, 'Project slug (falls back to .featureparity.yml)'
25
+ help_flag :body, 'The comment text', required: true
26
+ help_flag :change, 'Target a specific change id (defaults to the open proposal)'
27
+ help_flag :source_type, "Origin type: #{VALID_SOURCE_TYPES.join(', ')}"
28
+ help_flag :source_url, 'Origin URL'
29
+ help_flag :source_context, 'Free-form origin note'
30
+ help_example 'fp comment print_qr --body "This conflicts with the freezer label requirement" --project stowzilla'
31
+
22
32
  def run(args)
23
33
  opts, positional = parse_flags(args, KNOWN_FLAGS)
24
34
  require_project!(opts)
@@ -5,6 +5,15 @@ module Fp
5
5
  # fp config get|set|unset|list
6
6
  # Manage global CLI settings (stored in ~/.config/fp/config.yml)
7
7
  class ConfigCmd < Base
8
+ help_summary 'Manage global CLI settings (stored in ~/.config/fp/config.yml)'
9
+ help_usage 'fp config set <key> <value>',
10
+ 'fp config get <key>',
11
+ 'fp config unset <key>',
12
+ 'fp config list'
13
+ help_example 'fp config set api_url https://api.featureparity.dev'
14
+ help_example 'fp config list'
15
+ help_note "Allowed settings: api_url. 'fp config whoami' is an alias for 'fp whoami'."
16
+
8
17
  ALLOWED_SETTINGS = %w[api_url].freeze
9
18
 
10
19
  def run(args)
@@ -17,6 +17,11 @@ module Fp
17
17
  # 4. Agent runs `fp conversation <id>` to read the full context
18
18
  # 5. Agent implements based on Pari's suggestions
19
19
  class Conversation < Base
20
+ help_summary 'Read a Pari conversation so an agent can review AI suggestions'
21
+ help_usage 'fp conversation <conversation-id>'
22
+ help_example 'fp conversation 03h0abc123xyz'
23
+ help_note 'No --project needed; works for project and account-level conversations.'
24
+
20
25
  def run(args)
21
26
  _opts, positional = parse_flags(args)
22
27
  conversation_id = positional.first
@@ -11,6 +11,16 @@ module Fp
11
11
  # attached to a release as a point-in-time record of what the product is
12
12
  # supposed to do.
13
13
  class Export < Base
14
+ help_summary 'Export all requirements as a single Markdown document'
15
+ help_usage 'fp export --project <slug> [--status S] [--category C] [--surface X] [--output FILE]'
16
+ help_flag :project, 'Project slug (falls back to .featureparity.yml)'
17
+ help_flag :status, 'Only requirements with this status'
18
+ help_flag :category, 'Only requirements in this category'
19
+ help_flag :surface, 'Only requirements that name this surface'
20
+ help_flag :output, 'Write to this file instead of stdout'
21
+ help_example 'fp export --project stowzilla > requirements.md'
22
+ help_example 'fp export --project stowzilla --status active --surface customer_android --output reqs.md'
23
+
14
24
  KNOWN_FLAGS = {
15
25
  project: :string,
16
26
  status: :string,
@@ -2,37 +2,70 @@
2
2
 
3
3
  module Fp
4
4
  module Commands
5
- # fp help
5
+ # fp help [<command>]
6
+ #
7
+ # With no argument, prints the top-level usage. With a command name, prints that
8
+ # command's own help — the same page `fp <command> --help` shows (Fizzy #1489).
6
9
  class Help < Base
7
- def run(_args)
10
+ def run(args)
11
+ topic = args.first
12
+ if topic && Fp::CLI::COMMANDS.key?(topic)
13
+ puts Fp::CLI::COMMANDS[topic].help_text(topic)
14
+ return
15
+ end
16
+
17
+ if topic && !topic.start_with?('-')
18
+ output.error("Unknown command: #{topic}") if output
19
+ warn "Unknown command: #{topic}" unless output
20
+ puts "\nRun 'fp help' for the list of commands."
21
+ return
22
+ end
23
+
24
+ print_overview
25
+ end
26
+
27
+ private
28
+
29
+ def print_overview
8
30
  puts <<~HELP
9
31
  fp - FeatureParity CLI for agents
10
32
 
11
33
  USAGE
12
34
  fp <command> [flags]
35
+ fp <command> --help Show a command's own usage and flags
36
+ fp <command> --help --json ...as a machine-readable flag schema
37
+ fp help <command> Same, spelled the other way
38
+ fp --version Show the installed version (alias: -v)
13
39
 
14
40
  COMMANDS
15
41
  init --project <slug> Onboard this repo: config, CI gate, agent docs
16
42
  projects List projects you have access to
17
43
  projects show <slug> View a project and its surfaces
18
44
  projects create <slug> --name ... Create a project (owned by your key's owner)
19
- projects edit <slug> --name ... Update a project's name or description
45
+ projects edit <slug> --name ... Update a project's name, description, or showroom welcome
20
46
  projects delete <slug> [--yes] Delete a project (asks you to type its slug)
21
47
  surfaces --project <slug> List surfaces for a project
22
48
  surfaces show <key> --project ... View a single surface's details
23
49
  surfaces add <key> --project ... Create a surface
24
- surfaces edit <key> --project ... Change a surface (name, kind, repo, key...)
50
+ surfaces edit <key> --project ... Change a surface (name, kind, repo, key, showroom card...)
25
51
  surfaces delete <key> --project . Delete a surface (asks first; --yes to skip)
52
+ showroom --project <slug> Preview the client showroom as a client sees it
53
+ showroom media --project <slug> List the showroom's screenshots and videos
54
+ showroom media add <file> ... Add an image/video to a surface card (--surface) or the gallery
55
+ showroom media edit <id> ... Change a media item's --caption or --position
56
+ showroom media remove <id> --yes Take a media item off the showroom
26
57
  list --project <slug> List active requirements
27
58
  show <slug> --project <slug> Show requirement details
28
59
  propose --project <slug> ... Propose a new requirement (as draft)
29
60
  propose-change <slug> ... Propose a change to an existing requirement
30
61
  propose-change <slug> --removal Propose deleting a requirement
62
+ propose-change <slug> --no-parent Propose un-nesting a child to top level
63
+ propose-change <slug> --withdraw Withdraw your own open proposal (to amend it)
31
64
  comment <slug> --body "..." Comment on a requirement's open change proposal
32
65
  history <slug> --project <slug> Show a requirement's change history + diffs
33
66
  analyze [<slug>] --project <slug> Triage requirement quality (read-only; never proposes)
34
67
  changes [--source-url URL] [--github-pr] Show or publish work-item-linked activity
35
- attach <slug> <file> Upload a file attachment to a requirement
68
+ attach <slug> <file> [<file>...] Upload one or more file attachments to a requirement
36
69
  attachments <slug> List attachments for a requirement
37
70
  attachments <slug> <id> Get download URL for an attachment
38
71
  attach rm <slug> <id> Delete an attachment
@@ -51,7 +84,7 @@ module Fp
51
84
  whoami Show the resolved config (profile, env, API URL, key)
52
85
  repos set|get|list|unset Map surfaces to local repo paths
53
86
  setup Interactive first-time setup
54
- version Show version
87
+ version Show version (also: fp --version, fp -v)
55
88
  help Show this help
56
89
 
57
90
  GLOBAL FLAGS
@@ -66,6 +99,27 @@ module Fp
66
99
  so a checkout declares its own project once instead of every CI job and agent
67
100
  repeating it. `fp init` writes that file.
68
101
 
102
+ CLIENT SHOWROOM
103
+ The showroom is the one page a client sees: a welcome note, then a card per
104
+ surface with a link, a button label, and sign-in instructions. The team's
105
+ agents keep that content, because the agent that deployed a surface and made
106
+ its demo accounts is the one that knows the URL and the credentials:
107
+
108
+ fp projects edit <slug> --showroom-welcome "Hi Sam, here's what we built." \\
109
+ # or --showroom-welcome-file welcome.txt
110
+ fp surfaces edit web --showroom-url https://dev.example.com \\
111
+ --showroom-label "Open the web app" \\
112
+ --showroom-instructions-file docs/showroom/web.txt
113
+ fp showroom media add e2e/checkout.webm --surface web --caption "Checking out"
114
+ fp showroom media add overview.png # no --surface: the gallery under the welcome
115
+ fp showroom # what the client will see, and which surfaces are hidden
116
+
117
+ A surface is shown once it has a link, instructions, or media; pass an empty
118
+ value to clear a field (`--showroom-url ""`) and a surface with none drops out.
119
+ Media is images and video only (png, jpg, gif, webp, mp4, webm, mov; 100 MB).
120
+ Leaving feedback and turning the share link on or off are for humans, in the
121
+ web app under Project Settings → Showroom.
122
+
69
123
  ENFORCEMENT
70
124
  `fp check` fails a build when the project violates its enforcement level. Make it
71
125
  a required status check on your default branch to block merges.
@@ -287,6 +341,15 @@ module Fp
287
341
  --removal \\
288
342
  --reason "Superseded by print_container_label"
289
343
 
344
+ # Un-nest a child back to the top level (clears its parent)
345
+ fp propose-change print_container_qr_ios --project stowzilla --no-parent \\
346
+ --reason "Promoted to a top-level promise"
347
+
348
+ # Amend a proposal you already opened: the single-open-proposal rule won't let
349
+ # you open a second, so withdraw yours (restoring the requirement) and propose
350
+ # again. Only the author can withdraw; someone else's proposal goes to review.
351
+ fp propose-change print_container_qr --project stowzilla --withdraw
352
+
290
353
  # See what changed, why, and who asked for it
291
354
  fp history print_container_qr --project stowzilla
292
355
  fp history print_container_qr --project stowzilla --pending
@@ -357,6 +420,18 @@ module Fp
357
420
  --work-item https://app.fizzy.do/123/cards/456
358
421
  # --base-dir sets where relative test file paths in the report resolve from
359
422
  # Skipped tests are reported as 'stub'; everything else as 'present'.
423
+ # --test-type sets the kind of test (unit, e2e, ...) for markers that pin
424
+ # none. Give a different suite (e.g. a Playwright e2e run) its own type, or
425
+ # evidence for the same requirement+surface collides on the 'unspecified'
426
+ # bucket and the last report silently overwrites the earlier ones.
427
+
428
+ # Playwright's JUnit reporter is understood natively: titles joined with
429
+ # › (or :: for pytest-style ids) bind by name, a classname relative to the
430
+ # runner's testDir is searched for under base-dir, and --attach-media uploads
431
+ # the videos/screenshots the run recorded ([[ATTACHMENT|…]]) as visual
432
+ # evidence, each nested under the test it proves.
433
+ fp report --junit junit.xml --project stowzilla \\
434
+ --surface web --repo stowzilla/app --base-dir frontend --attach-media
360
435
 
361
436
  # CI ONLY: ingest real pass/fail results. Run in your CI pipeline after
362
437
  # the suite (on green AND red builds). Unlike `fp report`, this writes
@@ -9,6 +9,14 @@ module Fp
9
9
  # proposal, if there is one, is called out at the end because that's the thing
10
10
  # someone still has to decide on.
11
11
  class History < Base
12
+ help_summary "A requirement's change history: what changed, who changed it, and why"
13
+ help_usage 'fp history <slug> --project <slug> [--pending] [--limit N]'
14
+ help_flag :project, 'Project slug (falls back to .featureparity.yml)'
15
+ help_flag :pending, 'Show only the open proposal, if any'
16
+ help_flag :limit, 'Cap the number of events shown'
17
+ help_example 'fp history print_container_qr --project stowzilla'
18
+ help_example 'fp history print_container_qr --project stowzilla --pending'
19
+
12
20
  KNOWN_FLAGS = { project: :string, pending: :boolean, limit: :string }.freeze
13
21
 
14
22
  # Human labels for event types. The wire values are stable identifiers; these are
@@ -23,6 +23,19 @@ module Fp
23
23
  # unless --force, and the AGENTS.md section is appended once and detected by a
24
24
  # marker comment on re-run.
25
25
  class Init < Base
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]'
28
+ help_flag :project, 'Project slug to record in .featureparity.yml', required: true
29
+ help_flag :surface, 'Default surface for this (single-surface) repo'
30
+ help_flag :level, 'Enforcement level to record (usually omit and let the project decide)'
31
+ help_flag :dir, 'Target directory (defaults to the working directory)'
32
+ help_flag :force, 'Refresh files fp previously wrote'
33
+ help_flag :no_workflow, 'Skip writing the CI workflow'
34
+ help_flag :no_agents, 'Skip appending the AGENTS.md section'
35
+ help_example 'fp init --project stowzilla --surface api'
36
+ 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.'
38
+
26
39
  KNOWN_FLAGS = {
27
40
  project: :string,
28
41
  surface: :string,
@@ -174,6 +187,9 @@ module Fp
174
187
  puts ' 2. Add the FP_API_KEY secret to the repo so CI can report evidence and run the gate.'
175
188
  puts " 3. Try the gate locally: fp check#{opts[:surface] ? " --surface #{opts[:surface]}" : ''}"
176
189
  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.'
192
+ puts
177
193
  puts 'The gate starts at the `regression` level, which only fails on tests that have'
178
194
  puts 'gone red — it will not block you on requirements that were never covered. Raise'
179
195
  puts 'the level to `covered` or `strict` once the gaps are paid down.'
@@ -218,6 +234,13 @@ module Fp
218
234
  permissions:
219
235
  contents: read
220
236
  pull-requests: write
237
+ # Whether the FP_API_KEY secret exists on this repo. Every step that talks
238
+ # to FeatureParity is guarded on this, so the very first push — before
239
+ # anyone has added the secret — explains what to do instead of dying with a
240
+ # bare "No API key found" from fp. Fork pull requests also can't read the
241
+ # secret, and are skipped the same way.
242
+ env:
243
+ HAVE_FP_KEY: ${{ secrets.FP_API_KEY != '' }}
221
244
  steps:
222
245
  - uses: actions/checkout@v4
223
246
 
@@ -228,8 +251,30 @@ module Fp
228
251
  - name: Install fp
229
252
  run: gem install featureparity
230
253
 
254
+ # Without the secret there is nothing the gate can do yet, so say so
255
+ # clearly (in the job summary and the log) and let the build pass. This is
256
+ # the expected state right after `fp init`: the repo is onboarded, the
257
+ # maintainer just hasn't added the key. A green, explanatory run beats a
258
+ # red one nobody can action.
259
+ - name: FeatureParity not configured yet
260
+ if: env.HAVE_FP_KEY != 'true'
261
+ run: |
262
+ {
263
+ echo '## FeatureParity gate skipped'
264
+ echo ''
265
+ echo 'The `FP_API_KEY` secret is not set on this repository, so the parity'
266
+ echo 'gate cannot run yet. This is expected on the first push after `fp init`.'
267
+ echo ''
268
+ echo 'To enable the gate, add a repository secret named `FP_API_KEY`'
269
+ echo '(Settings → Secrets and variables → Actions → New repository secret).'
270
+ echo 'Mint a key at https://featureparity.dev (Account → Agents & API keys).'
271
+ echo ''
272
+ echo 'Fork pull requests cannot read the secret and will always show this.'
273
+ } >> "$GITHUB_STEP_SUMMARY"
274
+ echo 'FP_API_KEY is not set — skipping the parity gate. See the job summary.'
275
+
231
276
  - name: Update PR requirement changes
232
- if: github.event_name == 'pull_request'
277
+ if: github.event_name == 'pull_request' && env.HAVE_FP_KEY == 'true'
233
278
  env:
234
279
  FP_API_KEY: ${{ secrets.FP_API_KEY }}
235
280
  GITHUB_TOKEN: ${{ github.token }}
@@ -247,6 +292,9 @@ module Fp
247
292
  # Add `--warn-only` while adopting if you need the annotations without
248
293
  # blocking merges yet.
249
294
  #
295
+ # Guarded on HAVE_FP_KEY so a repo that hasn't added the secret yet gets
296
+ # the explanatory step above instead of a bare "No API key found" failure.
297
+ #
250
298
  # NOTE: as written this gates on what has been *recorded*, which on a pull
251
299
  # request is the default branch's state. To judge a branch on its own
252
300
  # results, emit JUnit in this job and pass it — nothing is saved:
@@ -258,6 +306,7 @@ module Fp
258
306
  # suite stays green while the requirement loses its only proof. Use one
259
307
  # call per surface.
260
308
  - name: Check parity
309
+ if: env.HAVE_FP_KEY == 'true'
261
310
  env:
262
311
  FP_API_KEY: ${{ secrets.FP_API_KEY }}
263
312
  run: fp check
@@ -5,6 +5,18 @@ module Fp
5
5
  # fp list --project <slug> [--surface X] [--category Y] [--gaps] [--status draft]
6
6
  # List requirements, optionally filtered
7
7
  class List < Base
8
+ help_summary 'List requirements, optionally filtered'
9
+ help_usage 'fp list --project <slug> [--surface X] [--category Y] [--tag Z] [--status S] [--gaps]'
10
+ help_flag :project, 'Project slug (falls back to .featureparity.yml)'
11
+ help_flag :surface, 'Only requirements that name this surface'
12
+ help_flag :category, 'Filter by category'
13
+ help_flag :tag, 'Filter by a free-form tag'
14
+ help_flag :status, 'Filter by status: active (default), draft, archived'
15
+ help_flag :gaps, 'Show only requirements with an uncovered required surface (reads evidence)'
16
+ help_example 'fp list --project stowzilla'
17
+ help_example 'fp list --project stowzilla --status draft', 'Drafts are hidden by default'
18
+ help_example 'fp list --project stowzilla --gaps --surface customer_android'
19
+
8
20
  KNOWN_FLAGS = {
9
21
  project: :string,
10
22
  surface: :string,
@@ -13,6 +13,15 @@ module Fp
13
13
  # parity report, which is the same computation the CI gate blocks on, so what you
14
14
  # see here is what CI will decide.
15
15
  class Matrix < Base
16
+ help_summary 'Show the parity matrix (requirements × surfaces with evidence state)'
17
+ help_usage 'fp matrix --project <slug> [--surface api] [--level covered] [--csv]'
18
+ help_flag :project, 'Project slug (falls back to .featureparity.yml)'
19
+ help_flag :surface, 'Limit to one surface'
20
+ help_flag :level, 'Evaluate against this enforcement level (off/regression/covered/strict)'
21
+ help_flag :csv, 'Emit CSV instead of the ASCII table'
22
+ help_example 'fp matrix --project stowzilla'
23
+ help_example 'fp matrix --project stowzilla --csv > matrix.csv'
24
+
16
25
  KNOWN_FLAGS = {
17
26
  project: :string,
18
27
  surface: :string,
@@ -5,6 +5,15 @@ module Fp
5
5
  # fp profile add|list
6
6
  # Manage named profiles for API authentication
7
7
  class Profile < Base
8
+ help_summary 'Manage named authentication profiles'
9
+ help_usage 'fp profile add <name> --api-key fp_... [--api-url URL] [--environment ENV]',
10
+ 'fp profile list'
11
+ help_flag :api_key, 'API key for the profile (starts with fp_)'
12
+ help_flag :api_url, 'Override API URL for the profile'
13
+ help_flag :environment, 'Named environment for the profile'
14
+ help_example 'fp profile add pari --api-key fp_...'
15
+ help_example 'fp --profile pari projects'
16
+
8
17
  KNOWN_FLAGS = {
9
18
  api_key: :string,
10
19
  api_url: :string,
@@ -17,7 +17,27 @@ module Fp
17
17
  # or takes --yes to skip the prompt, and a non-interactive session without --yes is
18
18
  # refused rather than guessed at. The server adds its own check — only an owner (or an
19
19
  # owner's key) may delete.
20
+ #
21
+ # `edit --showroom-welcome` keeps the note at the top of the client showroom (Fizzy
22
+ # #1479); the per-surface cards are `fp surfaces edit --showroom-*`, and `fp showroom`
23
+ # previews the result.
20
24
  class Projects < Base
25
+ help_summary 'List, view, create, edit, and delete projects'
26
+ help_usage 'fp projects',
27
+ 'fp projects show <slug>',
28
+ 'fp projects create <slug> --name "..." [--description ...] [--surfaces a,b,c]',
29
+ 'fp projects edit <slug> [--name ...] [--description ...]',
30
+ 'fp projects delete <slug> [--yes]'
31
+ help_flag :name, 'Project name (create/edit)'
32
+ help_flag :description, 'Project description (create/edit)'
33
+ help_flag :showroom_welcome, 'Welcome note at the top of the client showroom (edit; empty clears)'
34
+ help_flag :showroom_welcome_file, 'Read --showroom-welcome from a file (multi-line)'
35
+ help_flag :surfaces, 'Comma-separated surface keys to create with the project'
36
+ help_flag :yes, 'Skip the typed-slug confirmation on delete'
37
+ help_example 'fp projects create calib-rate --name "Calib Rate" --surfaces api,web'
38
+ help_example 'fp projects delete calib-rate --yes'
39
+ help_note 'Projects land on the key owner\'s account; the same key reaches them immediately. Delete is owner-only.'
40
+
21
41
  CREATE_FLAGS = {
22
42
  name: :string,
23
43
  description: :string,
@@ -26,7 +46,9 @@ module Fp
26
46
 
27
47
  EDIT_FLAGS = {
28
48
  name: :string,
29
- description: :string
49
+ description: :string,
50
+ showroom_welcome: :string,
51
+ showroom_welcome_file: :string
30
52
  }.freeze
31
53
 
32
54
  DELETE_FLAGS = {
@@ -135,14 +157,17 @@ module Fp
135
157
 
136
158
  def edit(args)
137
159
  opts, positional = parse_flags(args, EDIT_FLAGS)
138
- slug = require_slug!(positional, 'fp projects edit <slug> [--name "..."] [--description "..."]')
160
+ slug = require_slug!(positional, 'fp projects edit <slug> [--name "..."] [--description "..."] ' \
161
+ '[--showroom-welcome "..." | --showroom-welcome-file PATH]')
139
162
 
140
163
  params = {}
141
164
  params[:name] = opts[:name] if opts[:name]
142
165
  params[:description] = opts[:description] if opts.key?(:description)
166
+ welcome = showroom_welcome_param(opts)
167
+ params[:showroom_welcome] = welcome unless welcome.nil?
143
168
 
144
169
  if params.empty?
145
- output.error('Nothing to change. Pass --name and/or --description.')
170
+ output.error('Nothing to change. Pass --name, --description, --showroom-welcome, or --showroom-welcome-file.')
146
171
  exit 1
147
172
  end
148
173
 
@@ -161,9 +186,40 @@ module Fp
161
186
  puts
162
187
  puts " Name: #{record['name']}"
163
188
  puts " Description: #{blank?(record['description']) ? '(none)' : record['description']}"
189
+ if params.key?(:showroom_welcome)
190
+ puts " Showroom welcome: #{blank?(record['showroom_welcome']) ? '(cleared)' : ''}"
191
+ record['showroom_welcome'].to_s.each_line { |line| puts " #{line.rstrip}" }
192
+ end
164
193
  end
165
194
  end
166
195
 
196
+ # The welcome note from --showroom-welcome or --showroom-welcome-file, nil when
197
+ # neither was passed. An empty value clears the note. A file is the sane way to
198
+ # write several paragraphs; the showroom keeps the line breaks.
199
+ def showroom_welcome_param(opts)
200
+ if opts.key?(:showroom_welcome) && opts.key?(:showroom_welcome_file)
201
+ output.error('Pass --showroom-welcome or --showroom-welcome-file, not both.')
202
+ exit 1
203
+ end
204
+
205
+ if opts.key?(:showroom_welcome_file)
206
+ path = opts[:showroom_welcome_file]
207
+ if path == true || path.to_s.strip.empty?
208
+ output.error('--showroom-welcome-file needs a path')
209
+ exit 1
210
+ end
211
+ unless File.file?(path)
212
+ output.error("--showroom-welcome-file: no such file '#{path}'")
213
+ exit 1
214
+ end
215
+ return File.read(path).strip
216
+ end
217
+
218
+ return nil unless opts.key?(:showroom_welcome)
219
+
220
+ opts[:showroom_welcome] == true ? '' : opts[:showroom_welcome].to_s
221
+ end
222
+
167
223
  def delete(args)
168
224
  opts, positional = parse_flags(args, DELETE_FLAGS)
169
225
  slug = require_slug!(positional, 'fp projects delete <slug> [--yes]')
@@ -276,6 +332,10 @@ module Fp
276
332
  puts
277
333
  puts "Slug: #{project['slug']}"
278
334
  puts "Description: #{blank?(project['description']) ? '(none)' : project['description']}"
335
+ unless blank?(project['showroom_welcome'])
336
+ puts 'Showroom welcome:'
337
+ project['showroom_welcome'].to_s.each_line { |line| puts " #{line.rstrip}" }
338
+ end
279
339
  puts "Enforcement: #{project['enforcement_level']}" if project['enforcement_level']
280
340
  puts "Surfaces: #{surfaces.empty? ? '(none)' : surfaces.join(', ')}"
281
341
 
@@ -5,6 +5,24 @@ module Fp
5
5
  # fp propose --project <slug> --slug X --name "..." --why "..." --required a,b,c --required-types unit,e2e --acceptance "..." --parent <slug> --category <cat>
6
6
  # Create a new requirement as draft
7
7
  class Propose < Base
8
+ help_summary 'Propose a new requirement (created as a draft; a human activates it)'
9
+ help_usage 'fp propose --project <slug> --slug <key> --name "..." [flags]'
10
+ help_flag :project, 'Project slug (falls back to .featureparity.yml)'
11
+ help_flag :slug, 'Initial key for the requirement', required: true
12
+ help_flag :name, 'Human-readable title (one product promise, present tense)', required: true
13
+ help_flag :why, 'One or two sentences: who needs it, the problem, the outcome'
14
+ help_flag :acceptance, 'Observable result + boundaries a reviewer can check'
15
+ help_flag :required, 'Comma-separated surfaces this requirement must be covered on'
16
+ help_flag :required_types, "Comma-separated test kinds each surface needs (project's vocabulary)"
17
+ help_flag :parent, "Parent requirement's slug (nests one level; subset of parent's surfaces)"
18
+ help_flag :category, 'One of: functional, non-functional, ux, performance, security, compliance'
19
+ help_flag :tags, 'Comma-separated free-form labels'
20
+ help_flag :source_type, 'Where it came from: fizzy, discord, github_issue, jira, notion, manual, ai_chat'
21
+ help_flag :source_url, 'URL of the originating card/issue/thread'
22
+ help_flag :source_context, 'Free-form note about the origin'
23
+ help_example 'fp propose --project stowzilla --slug print_qr --name "Print QR on container label" --why "Staff mis-type the ID without a scannable code" --required api,customer_ios --acceptance "QR scans to the container detail page"'
24
+ help_note 'Agents can only create drafts. A human activates it in the web app.'
25
+
8
26
  KNOWN_FLAGS = {
9
27
  project: :string,
10
28
  slug: :string,