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.
- checksums.yaml +4 -4
- data/lib/fp/cli.rb +40 -0
- data/lib/fp/client.rb +70 -0
- data/lib/fp/commands/analyze.rb +10 -0
- data/lib/fp/commands/attach.rb +109 -46
- data/lib/fp/commands/attachments.rb +6 -0
- data/lib/fp/commands/base.rb +144 -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 +81 -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 +194 -0
- data/lib/fp/commands/showroom_media.rb +334 -0
- data/lib/fp/commands/surfaces.rb +142 -3
- data/lib/fp/commands/version.rb +3 -0
- data/lib/fp/commands/whoami.rb +6 -0
- data/lib/fp/commands.rb +2 -0
- data/lib/fp/junit.rb +213 -18
- data/lib/fp/version.rb +1 -1
- data/skills/fp/SKILL.md +45 -0
- data/skills/fp/references/cli.md +64 -1
- metadata +4 -2
data/lib/fp/commands/check.rb
CHANGED
|
@@ -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
|
|
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,
|
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,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(
|
|
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
|
|
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>
|
|
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
|
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,
|