featureparity 0.0.21 → 0.0.22
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 +40 -0
- data/lib/fp/client.rb +24 -0
- data/lib/fp/commands/analyze.rb +10 -0
- data/lib/fp/commands/attach.rb +109 -39
- data/lib/fp/commands/attachments.rb +6 -0
- data/lib/fp/commands/base.rb +136 -0
- data/lib/fp/commands/changed_tests.rb +12 -0
- data/lib/fp/commands/changes.rb +9 -0
- data/lib/fp/commands/check.rb +41 -3
- data/lib/fp/commands/ci_report.rb +17 -0
- data/lib/fp/commands/comment.rb +10 -0
- data/lib/fp/commands/config_cmd.rb +9 -0
- data/lib/fp/commands/conversation.rb +5 -0
- data/lib/fp/commands/export.rb +10 -0
- data/lib/fp/commands/help.rb +74 -6
- data/lib/fp/commands/history.rb +8 -0
- data/lib/fp/commands/init.rb +50 -1
- data/lib/fp/commands/list.rb +12 -0
- data/lib/fp/commands/matrix.rb +9 -0
- data/lib/fp/commands/profile.rb +9 -0
- data/lib/fp/commands/projects.rb +63 -3
- data/lib/fp/commands/propose.rb +18 -0
- data/lib/fp/commands/propose_change.rb +73 -2
- data/lib/fp/commands/report.rb +157 -7
- data/lib/fp/commands/repos.rb +11 -0
- data/lib/fp/commands/setup.rb +11 -0
- data/lib/fp/commands/show.rb +42 -3
- data/lib/fp/commands/showroom.rb +141 -0
- data/lib/fp/commands/surfaces.rb +116 -3
- data/lib/fp/commands/version.rb +3 -0
- data/lib/fp/commands/whoami.rb +6 -0
- data/lib/fp/commands.rb +1 -0
- data/lib/fp/junit.rb +213 -18
- data/lib/fp/version.rb +1 -1
- data/skills/fp/SKILL.md +27 -0
- data/skills/fp/references/cli.md +38 -1
- metadata +3 -2
data/lib/fp/commands/comment.rb
CHANGED
|
@@ -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
|
data/lib/fp/commands/export.rb
CHANGED
|
@@ -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,
|
data/lib/fp/commands/help.rb
CHANGED
|
@@ -2,37 +2,66 @@
|
|
|
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(
|
|
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
|
|
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
|
|
26
53
|
list --project <slug> List active requirements
|
|
27
54
|
show <slug> --project <slug> Show requirement details
|
|
28
55
|
propose --project <slug> ... Propose a new requirement (as draft)
|
|
29
56
|
propose-change <slug> ... Propose a change to an existing requirement
|
|
30
57
|
propose-change <slug> --removal Propose deleting a requirement
|
|
58
|
+
propose-change <slug> --no-parent Propose un-nesting a child to top level
|
|
59
|
+
propose-change <slug> --withdraw Withdraw your own open proposal (to amend it)
|
|
31
60
|
comment <slug> --body "..." Comment on a requirement's open change proposal
|
|
32
61
|
history <slug> --project <slug> Show a requirement's change history + diffs
|
|
33
62
|
analyze [<slug>] --project <slug> Triage requirement quality (read-only; never proposes)
|
|
34
63
|
changes [--source-url URL] [--github-pr] Show or publish work-item-linked activity
|
|
35
|
-
attach <slug> <file>
|
|
64
|
+
attach <slug> <file> [<file>...] Upload one or more file attachments to a requirement
|
|
36
65
|
attachments <slug> List attachments for a requirement
|
|
37
66
|
attachments <slug> <id> Get download URL for an attachment
|
|
38
67
|
attach rm <slug> <id> Delete an attachment
|
|
@@ -51,7 +80,7 @@ module Fp
|
|
|
51
80
|
whoami Show the resolved config (profile, env, API URL, key)
|
|
52
81
|
repos set|get|list|unset Map surfaces to local repo paths
|
|
53
82
|
setup Interactive first-time setup
|
|
54
|
-
version Show version
|
|
83
|
+
version Show version (also: fp --version, fp -v)
|
|
55
84
|
help Show this help
|
|
56
85
|
|
|
57
86
|
GLOBAL FLAGS
|
|
@@ -66,6 +95,24 @@ module Fp
|
|
|
66
95
|
so a checkout declares its own project once instead of every CI job and agent
|
|
67
96
|
repeating it. `fp init` writes that file.
|
|
68
97
|
|
|
98
|
+
CLIENT SHOWROOM
|
|
99
|
+
The showroom is the one page a client sees: a welcome note, then a card per
|
|
100
|
+
surface with a link, a button label, and sign-in instructions. The team's
|
|
101
|
+
agents keep that content, because the agent that deployed a surface and made
|
|
102
|
+
its demo accounts is the one that knows the URL and the credentials:
|
|
103
|
+
|
|
104
|
+
fp projects edit <slug> --showroom-welcome "Hi Sam, here's what we built." \\
|
|
105
|
+
# or --showroom-welcome-file welcome.txt
|
|
106
|
+
fp surfaces edit web --showroom-url https://dev.example.com \\
|
|
107
|
+
--showroom-label "Open the web app" \\
|
|
108
|
+
--showroom-instructions-file docs/showroom/web.txt
|
|
109
|
+
fp showroom # what the client will see, and which surfaces are hidden
|
|
110
|
+
|
|
111
|
+
A surface is shown once it has a link or instructions; pass an empty value
|
|
112
|
+
to clear a field (`--showroom-url ""`) and a surface with neither drops out.
|
|
113
|
+
Leaving feedback and turning the share link on or off are for humans, in the
|
|
114
|
+
web app under Project Settings → Showroom.
|
|
115
|
+
|
|
69
116
|
ENFORCEMENT
|
|
70
117
|
`fp check` fails a build when the project violates its enforcement level. Make it
|
|
71
118
|
a required status check on your default branch to block merges.
|
|
@@ -287,6 +334,15 @@ module Fp
|
|
|
287
334
|
--removal \\
|
|
288
335
|
--reason "Superseded by print_container_label"
|
|
289
336
|
|
|
337
|
+
# Un-nest a child back to the top level (clears its parent)
|
|
338
|
+
fp propose-change print_container_qr_ios --project stowzilla --no-parent \\
|
|
339
|
+
--reason "Promoted to a top-level promise"
|
|
340
|
+
|
|
341
|
+
# Amend a proposal you already opened: the single-open-proposal rule won't let
|
|
342
|
+
# you open a second, so withdraw yours (restoring the requirement) and propose
|
|
343
|
+
# again. Only the author can withdraw; someone else's proposal goes to review.
|
|
344
|
+
fp propose-change print_container_qr --project stowzilla --withdraw
|
|
345
|
+
|
|
290
346
|
# See what changed, why, and who asked for it
|
|
291
347
|
fp history print_container_qr --project stowzilla
|
|
292
348
|
fp history print_container_qr --project stowzilla --pending
|
|
@@ -357,6 +413,18 @@ module Fp
|
|
|
357
413
|
--work-item https://app.fizzy.do/123/cards/456
|
|
358
414
|
# --base-dir sets where relative test file paths in the report resolve from
|
|
359
415
|
# Skipped tests are reported as 'stub'; everything else as 'present'.
|
|
416
|
+
# --test-type sets the kind of test (unit, e2e, ...) for markers that pin
|
|
417
|
+
# none. Give a different suite (e.g. a Playwright e2e run) its own type, or
|
|
418
|
+
# evidence for the same requirement+surface collides on the 'unspecified'
|
|
419
|
+
# bucket and the last report silently overwrites the earlier ones.
|
|
420
|
+
|
|
421
|
+
# Playwright's JUnit reporter is understood natively: titles joined with
|
|
422
|
+
# › (or :: for pytest-style ids) bind by name, a classname relative to the
|
|
423
|
+
# runner's testDir is searched for under base-dir, and --attach-media uploads
|
|
424
|
+
# the videos/screenshots the run recorded ([[ATTACHMENT|…]]) as visual
|
|
425
|
+
# evidence, each nested under the test it proves.
|
|
426
|
+
fp report --junit junit.xml --project stowzilla \\
|
|
427
|
+
--surface web --repo stowzilla/app --base-dir frontend --attach-media
|
|
360
428
|
|
|
361
429
|
# CI ONLY: ingest real pass/fail results. Run in your CI pipeline after
|
|
362
430
|
# the suite (on green AND red builds). Unlike `fp report`, this writes
|
data/lib/fp/commands/history.rb
CHANGED
|
@@ -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
|
data/lib/fp/commands/init.rb
CHANGED
|
@@ -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
|
data/lib/fp/commands/list.rb
CHANGED
|
@@ -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,
|
data/lib/fp/commands/matrix.rb
CHANGED
|
@@ -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,
|
data/lib/fp/commands/profile.rb
CHANGED
|
@@ -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,
|
data/lib/fp/commands/projects.rb
CHANGED
|
@@ -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
|
|
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
|
|
data/lib/fp/commands/propose.rb
CHANGED
|
@@ -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,
|
|
@@ -10,11 +10,36 @@ module Fp
|
|
|
10
10
|
# asked for, and what prompted the ask. The requirement drops back to draft until a
|
|
11
11
|
# human reviews the diff — agents never approve their own changes.
|
|
12
12
|
class ProposeChange < Base
|
|
13
|
+
help_summary 'Propose a change to an existing requirement (or --removal to retire it)'
|
|
14
|
+
help_usage 'fp propose-change <slug> --project <slug> [--name/--why/--acceptance ...] --reason "..."',
|
|
15
|
+
'fp propose-change <slug> --removal --reason "..."'
|
|
16
|
+
help_flag :project, 'Project slug (falls back to .featureparity.yml)'
|
|
17
|
+
help_flag :name, 'Proposed new title'
|
|
18
|
+
help_flag :why, 'Proposed new Why'
|
|
19
|
+
help_flag :acceptance, 'Proposed new acceptance criteria'
|
|
20
|
+
help_flag :required, 'Proposed new required surfaces (comma-separated; cannot remove a surface)'
|
|
21
|
+
help_flag :required_types, 'Proposed new required test types (comma-separated)'
|
|
22
|
+
help_flag :category, 'Proposed new category: functional, non-functional, ux, performance, security, compliance'
|
|
23
|
+
help_flag :parent, "Set the parent requirement's slug"
|
|
24
|
+
help_flag :no_parent, 'Propose un-nesting the requirement back to the top level (clears --parent)'
|
|
25
|
+
help_flag :slug, 'Proposed new slug (evidence follows; markers still need updating)'
|
|
26
|
+
help_flag :reason, 'What prompted this change (use this, not --source-*, to record the prompt)'
|
|
27
|
+
help_flag :removal, 'Propose deleting the requirement instead of changing it'
|
|
28
|
+
help_flag :withdraw, 'Take back the open proposal you authored, restoring the requirement as it was'
|
|
29
|
+
help_flag :source_type, "Correct the requirement's recorded origin type (not the change's prompt)"
|
|
30
|
+
help_flag :source_url, "Correct the requirement's recorded origin URL"
|
|
31
|
+
help_flag :source_context, "Correct the requirement's recorded origin context"
|
|
32
|
+
help_example 'fp propose-change print_qr --project stowzilla --acceptance "QR scans after 24h at -20C" --reason "Cold-chain rollout"'
|
|
33
|
+
help_example 'fp propose-change print_qr --removal --reason "Superseded by print_container_label"'
|
|
34
|
+
help_note "Applies the change and drops the requirement to draft for a human to review. Use --reason\n" \
|
|
35
|
+
"to say what prompted it; --source-* overwrite the requirement's own origin fields."
|
|
36
|
+
|
|
13
37
|
KNOWN_FLAGS = {
|
|
14
38
|
project: :string, name: :string, why: :string, acceptance: :string,
|
|
15
39
|
required: :string, required_types: :string, category: :string, parent: :string, slug: :string,
|
|
40
|
+
no_parent: :boolean,
|
|
16
41
|
reason: :string, source_type: :string, source_url: :string, source_context: :string,
|
|
17
|
-
removal: :boolean
|
|
42
|
+
removal: :boolean, withdraw: :boolean
|
|
18
43
|
}.freeze
|
|
19
44
|
|
|
20
45
|
VALID_CATEGORIES = %w[functional non-functional ux performance security compliance].freeze
|
|
@@ -39,17 +64,21 @@ module Fp
|
|
|
39
64
|
exit 1
|
|
40
65
|
end
|
|
41
66
|
|
|
67
|
+
validate_flag_combinations!(opts)
|
|
42
68
|
validate_enums!(opts)
|
|
43
69
|
|
|
44
70
|
project_id = resolve_project_id(opts[:project])
|
|
45
71
|
requirement = find_requirement!(project_id, slug)
|
|
46
72
|
|
|
73
|
+
return withdraw!(requirement, slug) if opts[:withdraw]
|
|
74
|
+
|
|
47
75
|
params = build_params(opts, project_id)
|
|
48
76
|
removal = opts[:removal] ? true : false
|
|
49
77
|
|
|
50
78
|
if !removal && params.keys.none? { |k| proposed_field?(k) }
|
|
51
79
|
output.error('Nothing to propose. Pass at least one of --name/--slug/--why/--acceptance/' \
|
|
52
|
-
'--required/--required-types/--category/--parent, or --removal to
|
|
80
|
+
'--required/--required-types/--category/--parent/--no-parent, or --removal to ' \
|
|
81
|
+
'propose deletion.')
|
|
53
82
|
exit 1
|
|
54
83
|
end
|
|
55
84
|
|
|
@@ -68,6 +97,31 @@ module Fp
|
|
|
68
97
|
|
|
69
98
|
private
|
|
70
99
|
|
|
100
|
+
# Withdraw the requirement's own open proposal (Fizzy #1492). The single-open-
|
|
101
|
+
# proposal rule means an agent that wants to *amend* what it proposed can't just
|
|
102
|
+
# open a second one — it withdraws the first (restoring the requirement to what it
|
|
103
|
+
# was) and proposes again. The server only lets the proposal's author withdraw it.
|
|
104
|
+
def withdraw!(requirement, slug)
|
|
105
|
+
change_id = requirement['pending_change_id']
|
|
106
|
+
if change_id.nil? || change_id.to_s.empty?
|
|
107
|
+
output.error("Requirement '#{slug}' has no open proposal to withdraw.")
|
|
108
|
+
exit 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
result = client.withdraw_requirement_change(requirement['id'], change_id)
|
|
112
|
+
unless result[:ok]
|
|
113
|
+
output.error(result[:error], status: result[:status])
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
output.success(result[:data]) do
|
|
118
|
+
puts "Withdrew the open proposal for '#{slug}'."
|
|
119
|
+
puts
|
|
120
|
+
puts 'The requirement is restored to what it was before the proposal.'
|
|
121
|
+
puts 'To amend it, propose the change again with the corrected values.'
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
71
125
|
# A 409 means the requirement already has an open proposal — the one-at-a-time rule
|
|
72
126
|
# (two "what it was" snapshots would make a rollback ambiguous). Rather than a dead
|
|
73
127
|
# end, show what the open proposal is and point at `fp comment` so the agent can
|
|
@@ -109,6 +163,20 @@ module Fp
|
|
|
109
163
|
warn ' Fields: ' + changes.keys.join(', ')
|
|
110
164
|
end
|
|
111
165
|
|
|
166
|
+
# --parent and --no-parent contradict each other, and --withdraw is a standalone
|
|
167
|
+
# verb that ignores proposed values (Fizzy #1492).
|
|
168
|
+
def validate_flag_combinations!(opts)
|
|
169
|
+
if opts[:parent] && opts[:no_parent]
|
|
170
|
+
output.error('Pass either --parent <slug> or --no-parent, not both.')
|
|
171
|
+
exit 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
return unless opts[:withdraw] && opts[:removal]
|
|
175
|
+
|
|
176
|
+
output.error('--withdraw cancels an open proposal; it cannot be combined with --removal.')
|
|
177
|
+
exit 1
|
|
178
|
+
end
|
|
179
|
+
|
|
112
180
|
def validate_enums!(opts)
|
|
113
181
|
if opts[:category] && !VALID_CATEGORIES.include?(opts[:category])
|
|
114
182
|
output.error("Invalid category '#{opts[:category]}'. Must be one of: #{VALID_CATEGORIES.join(', ')}")
|
|
@@ -143,6 +211,9 @@ module Fp
|
|
|
143
211
|
params[:required_test_types] = opts[:required_types].split(',').map { |t| t.strip.downcase }.reject(&:empty?)
|
|
144
212
|
end
|
|
145
213
|
params[:parent_id] = resolve_parent_id(project_id, opts[:parent]) if opts[:parent]
|
|
214
|
+
# --no-parent un-nests a child back to top level: an empty parent_id. The server
|
|
215
|
+
# treats "" as "no parent" and diffs it against the current parent (Fizzy #1492).
|
|
216
|
+
params[:parent_id] = '' if opts[:no_parent]
|
|
146
217
|
|
|
147
218
|
params[:removal] = true if opts[:removal]
|
|
148
219
|
params[:reason] = opts[:reason] if opts[:reason]
|