featureparity 0.0.24 → 0.0.25
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/client.rb +14 -2
- data/lib/fp/commands/check.rb +190 -18
- data/lib/fp/commands/ci_report.rb +17 -7
- data/lib/fp/commands/init.rb +7 -0
- data/lib/fp/commands/show.rb +6 -0
- data/lib/fp/version.rb +1 -1
- data/skills/fp/SKILL.md +47 -0
- data/skills/fp/references/cli.md +43 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7a4558cdeaa90d0b4197daca351b9eadbeee5af21cb8454e9a1f44b3725bbaa
|
|
4
|
+
data.tar.gz: d0134aeca43dd6ee3415bfafe25ea7f67212eda1b8577a3bdfd9d9a3800aa150
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70827aadfd85037c992e670923ae4c7065c6ba913905028b903f4b6535f8498f530d3ec51a9ff245aa9734a3f044d6ca779b5e8b849b0a15d1b6b2aaed360e2f
|
|
7
|
+
data.tar.gz: 9ba46d5f8efa5a57e4b23c2557969cdd5c6b6697cb8584bb98b7c11fc237a099743c33474b46d05444361785278707981206af09e7c32570261ae5b104a9885d
|
data/lib/fp/client.rb
CHANGED
|
@@ -281,7 +281,7 @@ module Fp
|
|
|
281
281
|
# only do that by fetching the test files from GitHub, which fails silently on a
|
|
282
282
|
# private repo, so resolved results are always sent when we have them.
|
|
283
283
|
def ingest_ci_results(project_id, ci_url:, surface:, junit_xml:, pr_url: nil, results: nil,
|
|
284
|
-
test_revisions: nil, unmarked_failures: nil)
|
|
284
|
+
test_revisions: nil, unmarked_failures: nil, orphan_markers: nil)
|
|
285
285
|
body = {
|
|
286
286
|
ci_url: ci_url,
|
|
287
287
|
surface: surface,
|
|
@@ -296,6 +296,10 @@ module Fp
|
|
|
296
296
|
# Loose ends: failing tests with no fp:<slug> marker. The server stores them so the
|
|
297
297
|
# matrix can surface a red suite whose failures aren't bound to any requirement.
|
|
298
298
|
body[:unmarked_failures] = unmarked_failures if unmarked_failures && !unmarked_failures.empty?
|
|
299
|
+
# Orphan markers: fp:<slug> markers that bind to no requirement. Sent whenever the
|
|
300
|
+
# run could tell (an empty array included), so the server replaces the default
|
|
301
|
+
# branch's recorded set and a fixed marker drops off; nil means "couldn't tell".
|
|
302
|
+
body[:orphan_markers] = orphan_markers unless orphan_markers.nil?
|
|
299
303
|
|
|
300
304
|
post("/api/projects/#{project_id}/ci_results", body)
|
|
301
305
|
end
|
|
@@ -349,13 +353,21 @@ module Fp
|
|
|
349
353
|
# `results` are the run's outcomes already bound to their markers locally (see
|
|
350
354
|
# ingest_ci_results). Sending them is what lets a surface whose repo isn't registered
|
|
351
355
|
# — or a private one the server can't read — be previewed at all.
|
|
352
|
-
def preview_parity(project_id, surface:, junit_xmls:, level: nil, results: nil
|
|
356
|
+
def preview_parity(project_id, surface:, junit_xmls:, level: nil, results: nil,
|
|
357
|
+
pull_request: false, pr_url: nil)
|
|
353
358
|
# Older API deployments read `surface_filter` for report scoping while `surface`
|
|
354
359
|
# identifies the run's overlay. Send both during the rollout; current servers use
|
|
355
360
|
# `surface` for both meanings.
|
|
356
361
|
body = { surface: surface, surface_filter: surface, junit_xmls: Array(junit_xmls) }
|
|
357
362
|
body[:results] = results if results && !results.empty?
|
|
358
363
|
body[:level] = level if level
|
|
364
|
+
# A pull request's run is judged on what it changes, not on what the default branch
|
|
365
|
+
# already owes. Only sent when asked for, so a default-branch run stays the
|
|
366
|
+
# standing verdict.
|
|
367
|
+
if pull_request
|
|
368
|
+
body[:pull_request] = true
|
|
369
|
+
body[:pr_url] = pr_url if pr_url && !pr_url.to_s.empty?
|
|
370
|
+
end
|
|
359
371
|
post("/api/projects/#{project_id}/parity/preview", body)
|
|
360
372
|
end
|
|
361
373
|
|
data/lib/fp/commands/check.rb
CHANGED
|
@@ -35,18 +35,30 @@ module Fp
|
|
|
35
35
|
# that isn't clean yet — you get the annotations without blocking every merge on day
|
|
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
|
+
#
|
|
39
|
+
# `--pr [url]` (with --junit) asks the pull-request question instead of the standing
|
|
40
|
+
# one: "does this change make parity worse?" The server judges the run against the
|
|
41
|
+
# merged baseline and fails only on what the run introduced — a bound test gone red, a
|
|
42
|
+
# bound test that vanished, a non-green test added where there was none. Whatever
|
|
43
|
+
# master already owed is listed as inherited and not counted, so a PR that did
|
|
44
|
+
# everything right isn't red because a removal proposal is still waiting on a human.
|
|
45
|
+
# The default-branch gate (no --pr) keeps asking the standing question and stays red
|
|
46
|
+
# until someone decides — that is where that signal belongs (Fizzy #1506).
|
|
38
47
|
class Check < Base
|
|
39
48
|
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]'
|
|
49
|
+
help_usage 'fp check [--project <slug>] [--surface X] [--level L] [--junit FILE]... [--pr [URL]] [--warn-only]'
|
|
41
50
|
help_flag :project, 'Project slug (falls back to .featureparity.yml)'
|
|
42
51
|
help_flag :surface, 'Gate a single surface'
|
|
43
52
|
help_flag :test_type, 'Gate a single test type'
|
|
44
53
|
help_flag :level, 'Override the enforcement level for this run (off/regression/covered/strict)'
|
|
45
54
|
help_flag :junit, "Gate the branch on this run's results, overlaid on the grid (repeatable, one per surface)"
|
|
46
55
|
help_flag :base_dir, "Where the report's relative test-file paths resolve from"
|
|
56
|
+
help_flag :pr, 'With --junit: this run is a pull request — fail only on parity it makes worse, ' \
|
|
57
|
+
'not on what the default branch already owes (optionally the PR URL)'
|
|
47
58
|
help_flag :warn_only, 'Report violations but still exit 0'
|
|
48
59
|
help_example 'fp check', 'Gate on recorded state (project from .featureparity.yml)'
|
|
49
|
-
help_example 'fp check --surface api --junit junit.xml',
|
|
60
|
+
help_example 'fp check --surface api --junit junit.xml', 'Gate a default-branch run on its own results'
|
|
61
|
+
help_example 'fp check --surface api --junit junit.xml --pr', 'Gate a pull request on what it changes'
|
|
50
62
|
help_example 'fp check --warn-only --json'
|
|
51
63
|
help_note "Exit 0 = passed, 1 = violations (or request failed). The level lives on the project\n" \
|
|
52
64
|
'server-side; --level only overrides this one run.'
|
|
@@ -58,9 +70,17 @@ module Fp
|
|
|
58
70
|
level: :string,
|
|
59
71
|
junit: :string,
|
|
60
72
|
base_dir: :string,
|
|
73
|
+
# `--pr` alone is true; `--pr <url>` carries the URL. Not :boolean on purpose, so
|
|
74
|
+
# the parser takes a following value when one is given.
|
|
75
|
+
pr: :string,
|
|
61
76
|
warn_only: :boolean
|
|
62
77
|
}.freeze
|
|
63
78
|
|
|
79
|
+
# How many inherited violations to list before summarising the rest. They are
|
|
80
|
+
# informational — the gate already said they don't count — and on a project with a
|
|
81
|
+
# long standing backlog a full table buries the regressions that do.
|
|
82
|
+
INHERITED_PREVIEW_ROWS = 10
|
|
83
|
+
|
|
64
84
|
def run(args)
|
|
65
85
|
# Collect every --junit occurrence (parse_flags keeps only the last of a repeated
|
|
66
86
|
# flag), matching `fp ci-report`.
|
|
@@ -73,6 +93,13 @@ module Fp
|
|
|
73
93
|
surface = opts[:surface] || project_config.surface
|
|
74
94
|
level = opts[:level] || project_config.enforcement
|
|
75
95
|
|
|
96
|
+
# The pull-request question needs a run to compare: without --junit there is
|
|
97
|
+
# nothing to judge against the baseline.
|
|
98
|
+
if opts[:pr] && junit_paths.empty?
|
|
99
|
+
output.error('--pr needs --junit: a pull request is judged on its own run\'s results.')
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
|
|
76
103
|
project_id = resolve_project_id(project_slug)
|
|
77
104
|
|
|
78
105
|
result =
|
|
@@ -91,6 +118,13 @@ module Fp
|
|
|
91
118
|
violations = report['violations'] || []
|
|
92
119
|
passed = report['passing']
|
|
93
120
|
|
|
121
|
+
# An API that predates --pr ignores the flag and answers the standing question.
|
|
122
|
+
# Say so rather than let a reader believe the verdict was scoped to the change.
|
|
123
|
+
if opts[:pr] && !report['pull_request']
|
|
124
|
+
warn 'Warning: the API did not apply --pr (it predates pull-request mode); ' \
|
|
125
|
+
'this verdict counts every violation, including those the default branch already has.'
|
|
126
|
+
end
|
|
127
|
+
|
|
94
128
|
render(report, violations, passed, project_slug, opts)
|
|
95
129
|
|
|
96
130
|
# A failing gate is the whole point of the command, so it must be the exit code
|
|
@@ -130,7 +164,10 @@ module Fp
|
|
|
130
164
|
surface: surface,
|
|
131
165
|
junit_xmls: junit_paths.map { |p| File.read(p) },
|
|
132
166
|
level: level,
|
|
133
|
-
results: resolve_results(junit_paths, surface, opts)
|
|
167
|
+
results: resolve_results(junit_paths, surface, opts),
|
|
168
|
+
pull_request: !!opts[:pr],
|
|
169
|
+
# `--pr` alone parses as true; `--pr <url>` as the URL.
|
|
170
|
+
pr_url: (opts[:pr].is_a?(String) ? opts[:pr] : nil)
|
|
134
171
|
)
|
|
135
172
|
end
|
|
136
173
|
|
|
@@ -179,7 +216,13 @@ module Fp
|
|
|
179
216
|
scope = report['surface'] ? " (surface: #{report['surface']})" : ''
|
|
180
217
|
puts "FeatureParity gate: #{project_slug}#{scope}"
|
|
181
218
|
puts "Enforcement level: #{report['level']}"
|
|
182
|
-
if report['
|
|
219
|
+
if report['pull_request']
|
|
220
|
+
reported = Array(report['preview_surfaces']).join(', ')
|
|
221
|
+
puts "Mode: pull request — judging this run's results#{reported.empty? ? '' : " for #{reported}"} " \
|
|
222
|
+
'against what the default branch has recorded (nothing saved)'
|
|
223
|
+
puts ' Only parity this change makes worse fails the gate; what the default branch ' \
|
|
224
|
+
'already owes is listed, not counted.'
|
|
225
|
+
elsif report['preview']
|
|
183
226
|
reported = Array(report['preview_surfaces']).join(', ')
|
|
184
227
|
puts "Mode: preview — evaluating this run's results#{reported.empty? ? '' : " for #{reported}"} (nothing saved)"
|
|
185
228
|
end
|
|
@@ -198,10 +241,14 @@ module Fp
|
|
|
198
241
|
|
|
199
242
|
print_state_counts(summary)
|
|
200
243
|
print_scope_notice(report)
|
|
201
|
-
print_violations(violations)
|
|
244
|
+
print_violations(violations, report)
|
|
245
|
+
print_improvements(report)
|
|
246
|
+
print_unmeasured(report)
|
|
247
|
+
print_inherited(report)
|
|
202
248
|
print_orphans(report)
|
|
203
249
|
print_pending_reviews(report, project_slug)
|
|
204
250
|
print_unscoped_warning(summary, project_slug)
|
|
251
|
+
print_next_steps(report, violations)
|
|
205
252
|
print_verdict(passed, violations, opts, report)
|
|
206
253
|
end
|
|
207
254
|
end
|
|
@@ -252,10 +299,98 @@ module Fp
|
|
|
252
299
|
puts
|
|
253
300
|
end
|
|
254
301
|
|
|
255
|
-
def print_violations(violations)
|
|
302
|
+
def print_violations(violations, report = {})
|
|
256
303
|
return if violations.empty?
|
|
257
304
|
|
|
258
|
-
|
|
305
|
+
if report['pull_request']
|
|
306
|
+
puts "#{violations.size} regression(s) introduced by this change:"
|
|
307
|
+
else
|
|
308
|
+
puts "#{violations.size} violation(s):"
|
|
309
|
+
end
|
|
310
|
+
violations_table(violations)
|
|
311
|
+
puts
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Pull-request mode: what the default branch already owed. Listed so a reviewer can
|
|
315
|
+
# see why the standing gate is red and that this change didn't cause it; never
|
|
316
|
+
# counted, so never the reason this check fails. Capped — the regressions above are
|
|
317
|
+
# what the reader needs to act on, and a long backlog shouldn't bury them.
|
|
318
|
+
def print_inherited(report)
|
|
319
|
+
return unless report['pull_request']
|
|
320
|
+
|
|
321
|
+
inherited = report['inherited_violations'] || []
|
|
322
|
+
return if inherited.empty?
|
|
323
|
+
|
|
324
|
+
puts "#{inherited.size} violation(s) already on the default branch (not counted against this change):"
|
|
325
|
+
violations_table(inherited.first(INHERITED_PREVIEW_ROWS))
|
|
326
|
+
if inherited.size > INHERITED_PREVIEW_ROWS
|
|
327
|
+
puts " … and #{inherited.size - INHERITED_PREVIEW_ROWS} more. " \
|
|
328
|
+
'Run fp check without --junit to see the standing verdict in full.'
|
|
329
|
+
end
|
|
330
|
+
puts
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Pull-request mode: the cells this run fixed. The positive half of the verdict —
|
|
334
|
+
# an agent checking its own branch needs to see that the requirement it set out to
|
|
335
|
+
# cover went green, not only that nothing broke.
|
|
336
|
+
def print_improvements(report)
|
|
337
|
+
return unless report['pull_request']
|
|
338
|
+
|
|
339
|
+
fixed = report['improvements'] || []
|
|
340
|
+
return if fixed.empty?
|
|
341
|
+
|
|
342
|
+
puts "#{fixed.size} cell(s) this change fixed:"
|
|
343
|
+
output.table(
|
|
344
|
+
%w[SLUG SURFACE WAS NOW],
|
|
345
|
+
fixed.map do |f|
|
|
346
|
+
[f['slug'], [f['surface'], presence(f['test_type'])].compact.join(' / '),
|
|
347
|
+
"#{Parity.glyph(f['from'])} #{f['from']}", "#{Parity.glyph(f['to'])} #{f['to']}"]
|
|
348
|
+
end
|
|
349
|
+
)
|
|
350
|
+
puts
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Results bound to requirements the gate doesn't measure yet — drafts awaiting a
|
|
354
|
+
# human's activation. Without this, an agent that proposed a requirement and wrote
|
|
355
|
+
# its test would hear nothing about the one thing its branch was for.
|
|
356
|
+
def print_unmeasured(report)
|
|
357
|
+
unmeasured = report['unmeasured'] || []
|
|
358
|
+
return if unmeasured.empty?
|
|
359
|
+
|
|
360
|
+
puts "#{unmeasured.size} result(s) bound to draft requirement(s) — not measured until a human activates them:"
|
|
361
|
+
output.table(
|
|
362
|
+
%w[SLUG SURFACE RESULT],
|
|
363
|
+
unmeasured.map { |u| [u['slug'], u['surface'], "#{Parity.glyph(u['status'])} #{u['status']}"] }
|
|
364
|
+
)
|
|
365
|
+
puts
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# What to do next, grouped by who does it. The server names an action and an owner
|
|
369
|
+
# on every violation; this just reads them out so an agent asked to bring the
|
|
370
|
+
# project up to parity can take the `agent` rows and leave the `human` ones alone.
|
|
371
|
+
# On a pull request only the introduced violations are listed here — the inherited
|
|
372
|
+
# backlog is master's job, not this branch's.
|
|
373
|
+
def print_next_steps(report, violations)
|
|
374
|
+
rows = violations.select { |v| v['action'] }
|
|
375
|
+
rows += (report['orphans'] || []).reject { |o| o['inherited'] }.select { |o| o['action'] } if report['orphans_violate']
|
|
376
|
+
return if rows.empty?
|
|
377
|
+
|
|
378
|
+
puts 'Next steps:'
|
|
379
|
+
%w[agent human].each do |owner|
|
|
380
|
+
mine = rows.select { |v| v['owner'] == owner }
|
|
381
|
+
next if mine.empty?
|
|
382
|
+
|
|
383
|
+
puts " #{owner == 'agent' ? 'An agent can fix' : 'Waiting on a human'} (#{mine.size}):"
|
|
384
|
+
mine.group_by { |v| v['action'] }.each do |action, group|
|
|
385
|
+
slugs = group.map { |v| [v['slug'], v['surface']].compact.join('@') }.uniq
|
|
386
|
+
puts " #{action}: #{slugs.first(8).join(', ')}#{slugs.size > 8 ? ", … #{slugs.size - 8} more" : ''}"
|
|
387
|
+
puts " → #{group.first['action_hint']}" if group.first['action_hint']
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
puts
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def violations_table(violations)
|
|
259
394
|
# Show the TYPE column only when some requirement demands specific test types —
|
|
260
395
|
# otherwise it's an empty column of dashes on every project that doesn't use the
|
|
261
396
|
# feature.
|
|
@@ -275,7 +410,6 @@ module Fp
|
|
|
275
410
|
end
|
|
276
411
|
)
|
|
277
412
|
end
|
|
278
|
-
puts
|
|
279
413
|
end
|
|
280
414
|
|
|
281
415
|
# nil/blank → nil, so `presence(x)` reads as falsey in a guard.
|
|
@@ -293,20 +427,35 @@ module Fp
|
|
|
293
427
|
# as a violation when it counts and a note when it doesn't. Only the --junit path
|
|
294
428
|
# has a run to find these in; plain `fp check` reads recorded state and shows none.
|
|
295
429
|
def print_orphans(report)
|
|
296
|
-
|
|
297
|
-
return if
|
|
430
|
+
all = report['orphans'] || []
|
|
431
|
+
return if all.empty?
|
|
432
|
+
|
|
433
|
+
# On a pull request the default branch's own dangling markers are listed apart
|
|
434
|
+
# and never counted — this branch didn't add them.
|
|
435
|
+
inherited, orphans = all.partition { |o| o['inherited'] }
|
|
436
|
+
|
|
437
|
+
unless orphans.empty?
|
|
438
|
+
violates = report['orphans_violate']
|
|
439
|
+
lead = violates ? "#{orphans.size} orphan marker(s) — a violation at this level:" \
|
|
440
|
+
: "#{orphans.size} orphan marker(s) (not failing the gate at this level):"
|
|
441
|
+
puts lead
|
|
442
|
+
output.table(
|
|
443
|
+
%w[SLUG SURFACE WHERE],
|
|
444
|
+
orphans.map { |o| [o['slug'], o['surface'], marker_location(o)] }
|
|
445
|
+
)
|
|
446
|
+
puts 'These tests are marked fp:<slug> for a requirement that does not exist. ' \
|
|
447
|
+
'Either propose it (fp propose --slug <slug> ...) or fix the marker\'s slug — ' \
|
|
448
|
+
'FP will not invent a requirement from a marker.'
|
|
449
|
+
puts
|
|
450
|
+
end
|
|
298
451
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
puts lead
|
|
452
|
+
return if inherited.empty?
|
|
453
|
+
|
|
454
|
+
puts "#{inherited.size} orphan marker(s) already on the default branch (not counted against this change):"
|
|
303
455
|
output.table(
|
|
304
456
|
%w[SLUG SURFACE WHERE],
|
|
305
|
-
|
|
457
|
+
inherited.map { |o| [o['slug'], o['surface'], marker_location(o)] }
|
|
306
458
|
)
|
|
307
|
-
puts 'These tests are marked fp:<slug> for a requirement that does not exist. ' \
|
|
308
|
-
'Either propose it (fp propose --slug <slug> ...) or fix the marker\'s slug — ' \
|
|
309
|
-
'FP will not invent a requirement from a marker.'
|
|
310
459
|
puts
|
|
311
460
|
end
|
|
312
461
|
|
|
@@ -341,6 +490,29 @@ module Fp
|
|
|
341
490
|
# reads "Gate passed" as "the whole project is covered" (Fizzy #1490).
|
|
342
491
|
scope = report['scoped'] ? " for surface '#{report['surface']}' (other required surfaces not checked)" : ''
|
|
343
492
|
|
|
493
|
+
# A pull request's verdict is about the change, and the verdict line says so —
|
|
494
|
+
# "Gate passed" next to a red default branch would otherwise read as a
|
|
495
|
+
# contradiction. The baseline count tells the reviewer what master still owes.
|
|
496
|
+
if report['pull_request']
|
|
497
|
+
inherited = (report['inherited_violations'] || []).size
|
|
498
|
+
standing = inherited.positive? ? " #{inherited} violation(s) already on the default branch are not counted here." : ''
|
|
499
|
+
# Orphan markers the run added count too when the level says so; name them so a
|
|
500
|
+
# fail with no red cell doesn't read as "introduces 0 regressions".
|
|
501
|
+
added_orphans = report['orphans_violate'] ? (report['orphans'] || []).count { |o| !o['inherited'] } : 0
|
|
502
|
+
introduced = []
|
|
503
|
+
introduced << "#{violations.size} parity regression(s)" if violations.any? || added_orphans.zero?
|
|
504
|
+
introduced << "#{added_orphans} orphan marker(s)" if added_orphans.positive?
|
|
505
|
+
if passed
|
|
506
|
+
puts "Gate passed#{scope}: this change introduces no parity regressions.#{standing}"
|
|
507
|
+
elsif opts[:warn_only]
|
|
508
|
+
puts "Gate FAILED: this change introduces #{introduced.join(' and ')} — " \
|
|
509
|
+
'exiting 0 because --warn-only was given.'
|
|
510
|
+
else
|
|
511
|
+
puts "Gate FAILED: this change introduces #{introduced.join(' and ')}.#{standing}"
|
|
512
|
+
end
|
|
513
|
+
return
|
|
514
|
+
end
|
|
515
|
+
|
|
344
516
|
if passed
|
|
345
517
|
puts "Gate passed#{scope}."
|
|
346
518
|
elsif opts[:warn_only]
|
|
@@ -107,17 +107,23 @@ module Fp
|
|
|
107
107
|
repo = opts[:repo] || presence(ENV['GITHUB_REPOSITORY'])
|
|
108
108
|
|
|
109
109
|
totals = { created: 0, updated: 0, missing_failing: 0, reports: [], resolved: 0,
|
|
110
|
-
unmatched: 0, unknown: [], unmarked_failures: [], test_revisions: 0,
|
|
110
|
+
unmatched: 0, unknown: [], orphan_markers: [], unmarked_failures: [], test_revisions: 0,
|
|
111
111
|
findings_opened: 0, baselines_accepted: 0 }
|
|
112
112
|
|
|
113
113
|
resolved_reports = junit_paths.map do |path|
|
|
114
114
|
resolved = resolve_results(path, opts, commit_sha: commit_sha, repo: repo)
|
|
115
115
|
totals[:unmatched] += resolved[:unmatched]
|
|
116
116
|
|
|
117
|
-
results,
|
|
118
|
-
test_revisions,
|
|
117
|
+
results, orphans = partition_known(resolved[:results], known)
|
|
118
|
+
test_revisions, revision_orphans = partition_known(resolved[:test_revisions], known)
|
|
119
119
|
totals[:resolved] += results.length
|
|
120
|
-
totals[:unknown].concat(
|
|
120
|
+
totals[:unknown].concat(orphans.map { |r| r[:slug] }).concat(revision_orphans.map { |r| r[:slug] })
|
|
121
|
+
# The orphan markers themselves, with where they were found, for the server to
|
|
122
|
+
# record as the default branch's set (see Client#ingest_ci_results).
|
|
123
|
+
totals[:orphan_markers].concat(orphans.map do |r|
|
|
124
|
+
{ slug: r[:slug], surface: r[:surface], name: r[:title],
|
|
125
|
+
source_file: r[:source_file], source_line: r[:source_line] }
|
|
126
|
+
end)
|
|
121
127
|
totals[:unmarked_failures].concat(resolved[:unmarked_failures])
|
|
122
128
|
totals[:test_revisions] += test_revisions.length
|
|
123
129
|
|
|
@@ -142,7 +148,10 @@ module Fp
|
|
|
142
148
|
pr_url: opts[:pr],
|
|
143
149
|
results: combined_results,
|
|
144
150
|
test_revisions: combined_revisions,
|
|
145
|
-
unmarked_failures: totals[:unmarked_failures]
|
|
151
|
+
unmarked_failures: totals[:unmarked_failures],
|
|
152
|
+
# nil when the requirement list couldn't be fetched (known.nil?) — then nothing
|
|
153
|
+
# was partitioned and we must not tell the server "no orphans this run".
|
|
154
|
+
orphan_markers: known.nil? ? nil : totals[:orphan_markers].uniq
|
|
146
155
|
)
|
|
147
156
|
|
|
148
157
|
unless result[:ok]
|
|
@@ -261,11 +270,12 @@ module Fp
|
|
|
261
270
|
end.values
|
|
262
271
|
end
|
|
263
272
|
|
|
273
|
+
# Split results into those whose slug names a requirement and the orphans. Returns
|
|
274
|
+
# the orphan *entries* (not just slugs) so the run can report where each was found.
|
|
264
275
|
def partition_known(results, known)
|
|
265
276
|
return [results, []] if known.nil?
|
|
266
277
|
|
|
267
|
-
|
|
268
|
-
[kept, unknown.map { |r| r[:slug] }.uniq]
|
|
278
|
+
results.partition { |r| known.include?(r[:slug]) }
|
|
269
279
|
end
|
|
270
280
|
|
|
271
281
|
# Reports can be split into shards. Keep every independently identified testcase,
|
data/lib/fp/commands/init.rb
CHANGED
|
@@ -305,6 +305,13 @@ module Fp
|
|
|
305
305
|
# had its fp: marker stripped: the remaining tests still pass, so a plain
|
|
306
306
|
# suite stays green while the requirement loses its only proof. Use one
|
|
307
307
|
# call per surface.
|
|
308
|
+
#
|
|
309
|
+
# On a pull request add --pr so the verdict is about the change: only
|
|
310
|
+
# parity the branch makes worse fails it, and what the default branch
|
|
311
|
+
# already owes is listed, not counted. A push to the default branch gets
|
|
312
|
+
# no --pr and the standing verdict:
|
|
313
|
+
#
|
|
314
|
+
# fp check --surface <surface-key> --junit junit.xml --pr "${{ github.event.pull_request.html_url }}"
|
|
308
315
|
- name: Check parity
|
|
309
316
|
if: env.HAVE_FP_KEY == 'true'
|
|
310
317
|
env:
|
data/lib/fp/commands/show.rb
CHANGED
|
@@ -158,6 +158,12 @@ module Fp
|
|
|
158
158
|
puts " #{surface}:"
|
|
159
159
|
records.each do |e|
|
|
160
160
|
puts " Status: #{e['status']}"
|
|
161
|
+
# A pull request's CI may have written over the merged verdict; show both
|
|
162
|
+
# when they differ so "failing" next to "PR: …" reads as the branch's
|
|
163
|
+
# result, not the default branch's.
|
|
164
|
+
if e['merged_status'] && e['merged_status'] != e['status']
|
|
165
|
+
puts " Merged: #{e['merged_status']} (the default branch's last verdict)"
|
|
166
|
+
end
|
|
161
167
|
puts " Type: #{e['test_type']}" if e['test_type'] && e['test_type'] != 'unspecified'
|
|
162
168
|
if e['source_file']
|
|
163
169
|
loc = e['source_line'] ? "#{e['source_file']}:#{e['source_line']}" : e['source_file']
|
data/lib/fp/version.rb
CHANGED
data/skills/fp/SKILL.md
CHANGED
|
@@ -336,6 +336,53 @@ This also catches the one regression a green test suite can't: a test that was d
|
|
|
336
336
|
renamed, or had its `fp:` marker stripped. Every remaining test passes, so CI is happy,
|
|
337
337
|
while the requirement quietly lost its only proof.
|
|
338
338
|
|
|
339
|
+
**On a pull request, add `--pr`.** A push to the default branch is asked "is the project
|
|
340
|
+
in parity?" — the standing verdict, red until every pending proposal is decided. A pull
|
|
341
|
+
request is asked "does this change make parity worse?": only what the run introduced (a
|
|
342
|
+
bound test gone red, one that vanished, a non-green test added) fails it, and what the
|
|
343
|
+
default branch already owed is listed as "already on the default branch" and not counted.
|
|
344
|
+
So a PR that proposes a removal and deletes the test is not red for a review that hasn't
|
|
345
|
+
happened yet — but one that deletes a test under a *modification* proposal is: a
|
|
346
|
+
modification asks for the test to be rewritten, not removed.
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
fp check --surface api --junit junit.xml --pr "$PR_URL"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Verifying your own pull request.** Run the PR gate on your branch's JUnit and read three
|
|
353
|
+
things, all in `--json` too:
|
|
354
|
+
|
|
355
|
+
1. `violations` — regressions you introduced. Must be empty. Each names an `action`.
|
|
356
|
+
2. `improvements` — cells your change fixed (`from` → `to`). The requirement you set out
|
|
357
|
+
to cover should be here (or already `passing`).
|
|
358
|
+
3. `unmeasured` — your results bound to **draft** requirements. A test for a requirement
|
|
359
|
+
you proposed lands here until a human activates it; that is expected, not a gap.
|
|
360
|
+
|
|
361
|
+
`inherited_violations` are the default branch's, not yours. Leave them unless you were
|
|
362
|
+
asked to clear them.
|
|
363
|
+
|
|
364
|
+
**Bringing the default branch up to parity.** When asked (or on a schedule), run the
|
|
365
|
+
standing gate and work the list the server hands back:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
fp check --json # violations[*].action / .owner / .action_hint
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Take every violation whose `owner` is `agent`, grouped by `action`:
|
|
372
|
+
|
|
373
|
+
| action | what to do |
|
|
374
|
+
|--------|------------|
|
|
375
|
+
| `write_test` | write a test on that surface, mark it `fp:<slug>`, `fp report` it |
|
|
376
|
+
| `fix_test` | fix the red test — or, if the feature was removed on purpose, `fp propose-change <slug> --removal` |
|
|
377
|
+
| `replace_stub` | replace the stub with a real test |
|
|
378
|
+
| `run_in_ci` | the marker exists but CI never ran it green: make sure the test runs in a CI job that calls `fp ci-report` for that surface |
|
|
379
|
+
| `reverify` | the wording changed with no proposal open: check the test still proves it, fix it if not, re-report to clear `suspect` |
|
|
380
|
+
| `fix_marker` | an orphan marker: fix the slug, or `fp propose` the requirement |
|
|
381
|
+
|
|
382
|
+
Leave `owner: human` rows alone — `await_review` is a proposal a person has to decide,
|
|
383
|
+
`await_merge` is a pull request a person has to merge — and say so in your report. Open one
|
|
384
|
+
pull request per coherent batch, gate it with `--pr`, and re-run `fp check` after it merges.
|
|
385
|
+
|
|
339
386
|
### 9. Fill in the client showroom when a surface is deployed
|
|
340
387
|
|
|
341
388
|
The project's client (the person the software is being built for) sees one page: the
|
data/skills/fp/references/cli.md
CHANGED
|
@@ -526,6 +526,7 @@ fp check --surface api --junit junit.xml # gate on this run's results (preview)
|
|
|
526
526
|
| `--surface` | Gate one surface only |
|
|
527
527
|
| `--level` | Override the project's enforcement level |
|
|
528
528
|
| `--junit` | Evaluate this run's JUnit results without saving them (repeatable) |
|
|
529
|
+
| `--pr [url]` | With `--junit`: this run is a pull request — fail only on parity it makes worse |
|
|
529
530
|
| `--warn-only` | Report violations but exit 0 |
|
|
530
531
|
|
|
531
532
|
**Enforcement levels** (the level is configured on the project; don't hardcode one):
|
|
@@ -567,6 +568,48 @@ while the requirement silently lost its only proof. Pass one `--junit` per surfa
|
|
|
567
568
|
`--surface` tells the server which cells the run had a chance to cover, and pooling
|
|
568
569
|
surfaces into one call would misattribute vanished tests.
|
|
569
570
|
|
|
571
|
+
**Pull-request mode (`--pr`).** A push to the default branch asks "is the project in
|
|
572
|
+
parity?" — the standing verdict, red until every pending proposal is decided and every
|
|
573
|
+
gap filled. A pull request should be asked "does this change make parity worse?". With
|
|
574
|
+
`--pr` (and `--junit`) the run is judged against what the default branch has recorded and
|
|
575
|
+
only what it *introduced* fails: a bound test gone red, a bound test that vanished, a
|
|
576
|
+
non-green test added where there was none. What the default branch already owed is listed
|
|
577
|
+
as "already on the default branch" and not counted, so a PR that proposes a requirement's
|
|
578
|
+
removal and deletes its test is not red for the removal nobody has approved yet. Without
|
|
579
|
+
`--pr`, every PR on a `strict` project failed with master's own standing violations.
|
|
580
|
+
|
|
581
|
+
```
|
|
582
|
+
3 violation(s) already on the default branch (not counted against this change):
|
|
583
|
+
SLUG SURFACE STATE WHY
|
|
584
|
+
retire_old_heading api ⚠ suspect requirement changed; evidence needs re-verification
|
|
585
|
+
Gate passed: this change introduces no parity regressions. 3 violation(s) already on the default branch are not counted here.
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
A deleted bound test is only excused by a *removal* proposal. Under a modification
|
|
589
|
+
proposal (or a human's direct edit) the evidence is `suspect`, and a `suspect` test
|
|
590
|
+
missing from the run is flagged like a passing one that vanished — the proposal asked
|
|
591
|
+
for the test to be rewritten, not removed.
|
|
592
|
+
|
|
593
|
+
The PR verdict also lists what the change *fixed* (cells violating on the default branch,
|
|
594
|
+
green with the run applied) and results bound to *draft* requirements the gate doesn't
|
|
595
|
+
measure until a human activates them. In `--json`: `improvements`, `unmeasured`. Orphan
|
|
596
|
+
markers the default branch's last run already recorded are listed as "already on the
|
|
597
|
+
default branch" and never fail the PR; only orphans the run added count.
|
|
598
|
+
|
|
599
|
+
**Next steps.** Every verdict ends with the violations that count, grouped by who acts.
|
|
600
|
+
Each violation in `--json` carries `action`, `owner` (`agent` | `human`) and `action_hint`:
|
|
601
|
+
|
|
602
|
+
| action | owner | means |
|
|
603
|
+
|--------|-------|-------|
|
|
604
|
+
| `write_test` | agent | `missing` — no evidence on this surface |
|
|
605
|
+
| `fix_test` | agent | `failing` — red, or the marker vanished; or propose removal |
|
|
606
|
+
| `replace_stub` | agent | `stub` — a stub stands where a real test should |
|
|
607
|
+
| `run_in_ci` | agent | `present` — the marker exists but CI has never run it green |
|
|
608
|
+
| `reverify` | agent | `suspect` with no proposal open — the wording changed; re-check and re-report |
|
|
609
|
+
| `await_review` | human | `suspect` under an open proposal |
|
|
610
|
+
| `await_merge` | human | `pr_passing` — green on an open pull request |
|
|
611
|
+
| `fix_marker` | agent | an orphan marker — fix the slug or propose the requirement |
|
|
612
|
+
|
|
570
613
|
### fp init
|
|
571
614
|
|
|
572
615
|
Onboard a repo onto FeatureParity. Writes three files and makes no API calls (so it works
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featureparity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.25
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: net-http
|