featureparity 0.0.26 → 0.0.28
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 +3 -0
- data/lib/fp/client.rb +53 -2
- data/lib/fp/commands/analyze.rb +17 -0
- data/lib/fp/commands/base.rb +2 -0
- data/lib/fp/commands/comments.rb +146 -0
- data/lib/fp/commands/help.rb +48 -12
- data/lib/fp/commands/history.rb +12 -1
- data/lib/fp/commands/init.rb +35 -15
- data/lib/fp/commands/projects.rb +5 -1
- data/lib/fp/commands/propose.rb +34 -1
- data/lib/fp/commands/propose_change.rb +6 -6
- data/lib/fp/commands/report.rb +17 -1
- data/lib/fp/commands/show.rb +14 -3
- data/lib/fp/commands/spot_check.rb +458 -0
- data/lib/fp/commands/tutorial.rb +95 -0
- data/lib/fp/commands/whoami.rb +2 -0
- data/lib/fp/commands.rb +3 -0
- data/lib/fp/config.rb +12 -0
- data/lib/fp/junit.rb +14 -4
- data/lib/fp/tutorial/hexhaven/AGENTS.md +5 -0
- data/lib/fp/tutorial/hexhaven/README.md +29 -0
- data/lib/fp/tutorial/hexhaven/lib/hexhaven.rb +36 -0
- data/lib/fp/tutorial/hexhaven/test/junit_reporter.rb +134 -0
- data/lib/fp/tutorial/hexhaven/test/rules_test.rb +36 -0
- data/lib/fp/tutorial/hexhaven/test/test_helper.rb +6 -0
- data/lib/fp/version.rb +1 -1
- data/skills/fp/SKILL.md +27 -15
- data/skills/fp/references/cli.md +73 -0
- metadata +11 -2
data/lib/fp/junit.rb
CHANGED
|
@@ -756,10 +756,16 @@ module Fp
|
|
|
756
756
|
# How a test (or group) declaration announces its name, per language. Each entry is
|
|
757
757
|
# [regex, kind]; the captured group is the name. `:group` entries are describe-style
|
|
758
758
|
# blocks, whose name prefixes the names of the tests inside them.
|
|
759
|
+
#
|
|
760
|
+
# A quoted name may contain its own quote escaped (`'its kind\'s logo'`), so the name
|
|
761
|
+
# runs to the first *unescaped* closing quote and is unescaped before matching — the
|
|
762
|
+
# runner reports `its kind's logo`. Stopping at the first quote cut the name off at
|
|
763
|
+
# `kind\` and the marker silently bound nothing (Fizzy #1759).
|
|
764
|
+
QUOTED_NAME = %q{((?:\\\\.|(?!\\1).)+?)\\1}
|
|
759
765
|
TEST_DECL_PATTERNS = [
|
|
760
|
-
[/\b(?:it|test|bench)(?:\.\w+)*\s*\(\s*(['"`])
|
|
761
|
-
[/\bdescribe(?:\.\w+)*\s*\(\s*(['"`])
|
|
762
|
-
[/\b(?:it|specify|scenario)\s+(['"])
|
|
766
|
+
[/\b(?:it|test|bench)(?:\.\w+)*\s*\(\s*(['"`])#{QUOTED_NAME}/, :test], # vitest/jest/mocha
|
|
767
|
+
[/\bdescribe(?:\.\w+)*\s*\(\s*(['"`])#{QUOTED_NAME}/, :group], # vitest/jest/mocha group
|
|
768
|
+
[/\b(?:it|specify|scenario)\s+(['"])#{QUOTED_NAME}/, :test], # rspec
|
|
763
769
|
[/\b(?:def|fun)\s+`(.+?)`/, :test], # kotlin backticked
|
|
764
770
|
[/\b(?:def|fun|func)\s+(test_?\w+)/i, :test] # minitest / xctest / pytest
|
|
765
771
|
].freeze
|
|
@@ -779,7 +785,7 @@ module Fp
|
|
|
779
785
|
next unless match
|
|
780
786
|
|
|
781
787
|
# Two-quote patterns capture the quote first, the name second.
|
|
782
|
-
return [match.captures.last, kind]
|
|
788
|
+
return [unescape_name(match.captures.last), kind]
|
|
783
789
|
end
|
|
784
790
|
|
|
785
791
|
# A line of real code that declares nothing recognisable: the marker doesn't
|
|
@@ -794,5 +800,9 @@ module Fp
|
|
|
794
800
|
# the first line of real code regardless, so this only bounds how long a comment block
|
|
795
801
|
# between the marker and its test may be.
|
|
796
802
|
DECL_SEARCH_WINDOW = 40
|
|
803
|
+
|
|
804
|
+
def unescape_name(name)
|
|
805
|
+
name.gsub(/\\(.)/, '\\1')
|
|
806
|
+
end
|
|
797
807
|
end
|
|
798
808
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Hexhaven: your FeatureParity practice table
|
|
2
|
+
|
|
3
|
+
Hexhaven's game server: the referee that checks every move. Rules in `lib/`, tests in
|
|
4
|
+
`test/`. Plain Ruby; nothing to install.
|
|
5
|
+
|
|
6
|
+
1. Run the tests:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
ruby -Ilib test/rules_test.rb
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. Put this above the last test in `test/rules_test.rb`, or ask your agent to
|
|
13
|
+
(`AGENTS.md` tells it how):
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# fp:tallest_tower_scores_two_points@server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. Report it:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
JUNIT_OUT=junit.xml ruby -Ilib test/rules_test.rb
|
|
23
|
+
fp report --junit junit.xml --repo hexhaven/server
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The tallest-tower cell on Server turns from **missing** to **present**. Back to the
|
|
27
|
+
tutorial.
|
|
28
|
+
|
|
29
|
+
Made a mess? `fp tutorial --restart`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hexhaven — a made-up board game, and the rules its game server referees.
|
|
4
|
+
#
|
|
5
|
+
# Every rule here is a requirement in your FeatureParity tutorial project. The server checks
|
|
6
|
+
# every move; Web, iOS and Android each show the same rules on their own, and FeatureParity
|
|
7
|
+
# proves all four agree.
|
|
8
|
+
module Hexhaven
|
|
9
|
+
DIE = (1..6).freeze
|
|
10
|
+
HARBOR_RATE = 3
|
|
11
|
+
TALLEST_TOWER_MINIMUM = 5
|
|
12
|
+
TALLEST_TOWER_POINTS = 2
|
|
13
|
+
|
|
14
|
+
# A turn starts with two six-sided dice.
|
|
15
|
+
def self.roll(rng = Random.new)
|
|
16
|
+
[rng.rand(DIE), rng.rand(DIE)]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# At a harbor, three of one good buy one of any other.
|
|
20
|
+
def self.harbor_trade(hand, give:, get:)
|
|
21
|
+
raise ArgumentError, "a harbor trade needs #{HARBOR_RATE} #{give}" if hand.fetch(give, 0) < HARBOR_RATE
|
|
22
|
+
|
|
23
|
+
hand.merge(give => hand[give] - HARBOR_RATE, get => hand.fetch(get, 0) + 1)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Who holds the tallest-tower bonus, given each player's tallest tower in floors. The
|
|
27
|
+
# first tower of five floors or more takes it; only a strictly taller one steals it.
|
|
28
|
+
def self.tallest_tower(towers, holder: nil)
|
|
29
|
+
contenders = towers.select { |_, floors| floors >= TALLEST_TOWER_MINIMUM }
|
|
30
|
+
return nil if contenders.empty?
|
|
31
|
+
|
|
32
|
+
best = contenders.values.max
|
|
33
|
+
holder = nil unless holder && towers.fetch(holder, 0) == best
|
|
34
|
+
holder || contenders.key(best)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A dependency-free JUnit XML reporter for Minitest, written only when JUNIT_OUT is set.
|
|
4
|
+
#
|
|
5
|
+
# `fp report --junit` reads the report, opens each test's file, and finds the fp:<slug>
|
|
6
|
+
# marker above it. That is why every testcase carries the file and line it was declared
|
|
7
|
+
# on.
|
|
8
|
+
|
|
9
|
+
require 'minitest'
|
|
10
|
+
require 'fileutils'
|
|
11
|
+
|
|
12
|
+
module MinitestJUnitReporter
|
|
13
|
+
class Reporter < Minitest::AbstractReporter
|
|
14
|
+
def initialize(output_path, base_dir)
|
|
15
|
+
super()
|
|
16
|
+
@output_path = output_path
|
|
17
|
+
@base_dir = File.expand_path(base_dir)
|
|
18
|
+
@results = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def record(result)
|
|
22
|
+
@results << result
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def report
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(@output_path))
|
|
27
|
+
File.write(@output_path, xml)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def xml
|
|
33
|
+
failures = @results.count { |r| r.failures.any? { |f| !f.is_a?(Minitest::Skip) } }
|
|
34
|
+
skips = @results.count { |r| r.failures.any? { |f| f.is_a?(Minitest::Skip) } }
|
|
35
|
+
time = @results.sum(&:time)
|
|
36
|
+
|
|
37
|
+
body = @results.map { |r| testcase_xml(r) }.join
|
|
38
|
+
|
|
39
|
+
<<~XML
|
|
40
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
41
|
+
<testsuites>
|
|
42
|
+
<testsuite name="minitest" tests="#{@results.size}" failures="#{failures}" errors="0" skipped="#{skips}" time="#{format('%.6f', time)}">
|
|
43
|
+
#{body} </testsuite>
|
|
44
|
+
</testsuites>
|
|
45
|
+
XML
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def testcase_xml(result)
|
|
49
|
+
file, line = declaring_location(result)
|
|
50
|
+
attrs = [
|
|
51
|
+
%(name="#{esc(result.name)}"),
|
|
52
|
+
%(classname="#{esc(result.klass)}"),
|
|
53
|
+
%(file="#{esc(relative_to_base(file))}"),
|
|
54
|
+
# `line` is what lets `fp` bind each marker to the specific test below it. Without
|
|
55
|
+
# it fp can only handle files with exactly one marker.
|
|
56
|
+
(%(line="#{line}") if line),
|
|
57
|
+
%(time="#{format('%.6f', result.time)}")
|
|
58
|
+
].compact.join(' ')
|
|
59
|
+
|
|
60
|
+
skip = result.failures.find { |f| f.is_a?(Minitest::Skip) }
|
|
61
|
+
real_failures = result.failures.reject { |f| f.is_a?(Minitest::Skip) }
|
|
62
|
+
|
|
63
|
+
if skip
|
|
64
|
+
%( <testcase #{attrs}>\n <skipped message="#{esc(skip.message)}"/>\n </testcase>\n)
|
|
65
|
+
elsif real_failures.any?
|
|
66
|
+
inner = real_failures.map do |f|
|
|
67
|
+
tag = f.is_a?(Minitest::UnexpectedError) ? 'error' : 'failure'
|
|
68
|
+
%( <#{tag} message="#{esc(f.message)}"/>\n)
|
|
69
|
+
end.join
|
|
70
|
+
%( <testcase #{attrs}>\n#{inner} </testcase>\n)
|
|
71
|
+
else
|
|
72
|
+
%( <testcase #{attrs}/>\n)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Where the test method is declared, as [path, line]. The path is what `fp` opens to
|
|
77
|
+
# look for markers and the line is what binds a marker to its test, so this matters
|
|
78
|
+
# more than the rest of the report.
|
|
79
|
+
def declaring_location(result)
|
|
80
|
+
# Minitest 5.18+ exposes source_location on the result.
|
|
81
|
+
return result.source_location if result.respond_to?(:source_location)
|
|
82
|
+
|
|
83
|
+
# Older minitest: ask the test class for the method's definition site.
|
|
84
|
+
klass = Object.const_get(result.klass.to_s)
|
|
85
|
+
klass.instance_method(result.name).source_location || [nil, nil]
|
|
86
|
+
rescue StandardError
|
|
87
|
+
[nil, nil]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def relative_to_base(path)
|
|
91
|
+
return '' unless path
|
|
92
|
+
|
|
93
|
+
absolute = File.expand_path(path)
|
|
94
|
+
relative = absolute.delete_prefix("#{@base_dir}/")
|
|
95
|
+
relative == absolute ? path : relative
|
|
96
|
+
rescue StandardError
|
|
97
|
+
''
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def esc(str)
|
|
101
|
+
str.to_s
|
|
102
|
+
.gsub('&', '&').gsub('<', '<').gsub('>', '>')
|
|
103
|
+
.gsub('"', '"').gsub("'", ''')
|
|
104
|
+
.gsub(/[^[:print:]\t\n]/, '')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class << self
|
|
109
|
+
# Register as a Minitest plugin. Minitest calls plugin_<name>_init while building its
|
|
110
|
+
# composite reporter, which is the only point where a reporter can still be added.
|
|
111
|
+
# No-op unless JUNIT_OUT is set, so ordinary test runs are unaffected.
|
|
112
|
+
def install!
|
|
113
|
+
path = ENV['JUNIT_OUT']
|
|
114
|
+
return if path.nil? || path.strip.empty?
|
|
115
|
+
return if @installed
|
|
116
|
+
|
|
117
|
+
@installed = true
|
|
118
|
+
@output_path = File.expand_path(path)
|
|
119
|
+
@base_dir = ENV['JUNIT_BASE_DIR'] || Dir.pwd
|
|
120
|
+
|
|
121
|
+
Minitest.extensions << 'junit_xml' unless Minitest.extensions.include?('junit_xml')
|
|
122
|
+
|
|
123
|
+
Minitest.singleton_class.define_method(:plugin_junit_xml_init) do |_options|
|
|
124
|
+
Minitest.reporter << MinitestJUnitReporter.build_reporter
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_reporter
|
|
129
|
+
Reporter.new(@output_path, @base_dir)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
MinitestJUnitReporter.install!
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
# The referee's rules, each bound to the requirement it proves by the fp: comment directly
|
|
6
|
+
# above it. `@server` pins the surface, since this repo is the game server.
|
|
7
|
+
class RulesTest < Minitest::Test
|
|
8
|
+
# fp:turn_starts_with_two_dice@server
|
|
9
|
+
def test_a_turn_rolls_two_six_sided_dice
|
|
10
|
+
100.times do
|
|
11
|
+
dice = Hexhaven.roll
|
|
12
|
+
assert_equal 2, dice.size
|
|
13
|
+
dice.each { |die| assert_includes 1..6, die }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# fp:harbor_trades_three_for_one@server
|
|
18
|
+
def test_three_wool_become_one_brick_at_a_harbor
|
|
19
|
+
hand = Hexhaven.harbor_trade({ wool: 3 }, give: :wool, get: :brick)
|
|
20
|
+
|
|
21
|
+
assert_equal({ wool: 0, brick: 1 }, hand)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# fp:harbor_trades_three_for_one@server
|
|
25
|
+
def test_a_harbor_refuses_fewer_than_three
|
|
26
|
+
assert_raises(ArgumentError) { Hexhaven.harbor_trade({ wool: 2 }, give: :wool, get: :brick) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# 👉 Your move: put this line (uncommented) above the test, or ask your agent to.
|
|
30
|
+
# # fp:tallest_tower_scores_two_points@server
|
|
31
|
+
def test_a_six_floor_tower_takes_the_bonus_from_a_five_floor_tower
|
|
32
|
+
assert_equal :ada, Hexhaven.tallest_tower({ ada: 5, ben: 3 })
|
|
33
|
+
assert_equal :ben, Hexhaven.tallest_tower({ ada: 5, ben: 6 }, holder: :ada)
|
|
34
|
+
assert_nil Hexhaven.tallest_tower({ ada: 4, ben: 4 })
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/fp/version.rb
CHANGED
data/skills/fp/SKILL.md
CHANGED
|
@@ -202,23 +202,33 @@ Before drafting:
|
|
|
202
202
|
|
|
203
203
|
Write each field for a reviewer, not for the implementation:
|
|
204
204
|
|
|
205
|
-
- **Title:** one clear product promise in present tense
|
|
206
|
-
the project (`Dispatchers see current shipment
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
205
|
+
- **Title:** one clear product promise in present tense, with a concrete subject and active
|
|
206
|
+
verb. Name the capability or outcome, not the project (`Dispatchers see current shipment
|
|
207
|
+
status`, not `Implement status polling`).
|
|
208
|
+
- **Why:** one natural sentence saying who needs this, the problem or risk, and the outcome
|
|
209
|
+
worth protecting. Do not repeat the title, narrate the request or bug history, advertise
|
|
210
|
+
the feature, or explain how it will be built. Delete AI filler such as “ensure a seamless,
|
|
211
|
+
robust experience.”
|
|
212
|
+
- **Acceptance:** one independently verifiable rule of observable behaviour that lets someone
|
|
213
|
+
decide whether the promise is true. State the relevant trigger or situation, the result, and
|
|
214
|
+
material boundaries that qualify that same promise. Given/When/Then may clarify those parts, but it
|
|
215
|
+
is not a mandatory template. Split a second result that could fail independently into its
|
|
216
|
+
own requirement instead of comma-chaining it here.
|
|
217
|
+
- **Example:** supply one concise concrete case that would pass the general rule, and put it
|
|
218
|
+
after the rule in Acceptance as `Example: ...`. Use the domain's real terms and
|
|
219
|
+
representative data, but ask rather than invent consequential policy. Do not mistake a few
|
|
220
|
+
examples for the rule or let executable tests stand for every case; neither replaces the
|
|
221
|
+
general rule and continued conversation.
|
|
216
222
|
- **Numbers and policy:** include a number only when the source made it a real product decision
|
|
217
223
|
or an external constraint requires it. Do not invent implementation defaults and present
|
|
218
224
|
them as scope.
|
|
219
225
|
- **Implementation detail:** omit components, data models, algorithms, endpoints, polling
|
|
220
226
|
intervals, and UI controls unless that interface or constraint is itself user-visible policy.
|
|
221
227
|
|
|
228
|
+
State the promise itself rather than commentary about it. Avoid openings such as “This
|
|
229
|
+
feature…”, “This requirement…”, “The goal is…”, “Users should be able to…”, and “The
|
|
230
|
+
system should…”. Use the real actor or product as the subject and an active verb.
|
|
231
|
+
|
|
222
232
|
A good requirement is concise, not merely short. Keep every sentence that changes a decision;
|
|
223
233
|
remove everything that only sounds thorough.
|
|
224
234
|
|
|
@@ -230,10 +240,12 @@ remove everything that only sounds thorough.
|
|
|
230
240
|
**Acceptance:** `Use a background worker and refresh every 30 seconds.`
|
|
231
241
|
This invents policy, prescribes a design, and never states the outcome.
|
|
232
242
|
|
|
233
|
-
Before saving,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
243
|
+
Before saving, answer three questions against the actual fields: Who is this promise for? How
|
|
244
|
+
would the concrete example prove it done? Why does the outcome matter? If any answer is missing
|
|
245
|
+
or vague, revise the draft or ask the user. Then read it as a manager deciding scope: Is the
|
|
246
|
+
value clear? Can the result be observed? Are real product decisions distinguished from guesses?
|
|
247
|
+
Could every clause fail independently and, if so, should it be split? Does every sentence earn
|
|
248
|
+
its place? Do not create the record until the draft passes this review.
|
|
237
249
|
|
|
238
250
|
Then create the proposal as a draft:
|
|
239
251
|
|
data/skills/fp/references/cli.md
CHANGED
|
@@ -266,6 +266,12 @@ fp propose --project stowzilla \
|
|
|
266
266
|
| `--required` | No | Comma-separated surfaces that must implement this |
|
|
267
267
|
| `--acceptance` | No | Observable behaviour and material boundaries |
|
|
268
268
|
|
|
269
|
+
After creating the draft, `fp propose` prints its quality feedback — a verdict, the issues
|
|
270
|
+
found, and suggestions — in the same run (`analysis` in `--json`). Act on it: revise a weak
|
|
271
|
+
draft with `fp propose-change`, or withdraw one that shouldn't be a requirement. When analysis
|
|
272
|
+
is unavailable the draft is still created, and the output says the feedback couldn't be
|
|
273
|
+
produced (`analysis_error`).
|
|
274
|
+
|
|
269
275
|
**Write for a decision-maker, not an implementer.** The result should sound natural and be
|
|
270
276
|
ready for management review without someone translating AI boilerplate or reverse-engineering
|
|
271
277
|
the intended outcome. Treat it as the current record of an ongoing conversation, not a finished
|
|
@@ -370,6 +376,27 @@ the review decision where one was made. An open proposal is called out at the en
|
|
|
370
376
|
|
|
371
377
|
`--project` is optional when the repo has a `.featureparity.yml`.
|
|
372
378
|
|
|
379
|
+
### fp comments
|
|
380
|
+
|
|
381
|
+
Read or join the team's conversation on a requirement, and tag people or agents in it.
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
fp comments print_container_qr --project stowzilla # comments + who can be tagged
|
|
385
|
+
fp comments print_container_qr --body "The label redesign dropped this — retire it?" \
|
|
386
|
+
--tag andy@example.com
|
|
387
|
+
fp comments print_container_qr --body "Can you re-check this against the new label?" --tag claude
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
A comment never changes the requirement. `--tag` takes a teammate's email or name, or one of
|
|
391
|
+
the team's agents by name or key name; an unknown or ambiguous tag stops the command and
|
|
392
|
+
posts nothing. Tagged people get an email; each tagged agent fires the project's
|
|
393
|
+
`requirement.agent_mentioned` webhook, whose payload names the agent, the requirement and
|
|
394
|
+
the comment, plus the `fp` commands to read and answer it. If a human tagged you, answer
|
|
395
|
+
with `fp comments <slug> --body "..."` — and if the question means the requirement is
|
|
396
|
+
wrong, propose the change with `fp propose-change` rather than settling it in a comment.
|
|
397
|
+
|
|
398
|
+
`fp comment` (singular) is different: it threads a comment onto an open change proposal.
|
|
399
|
+
|
|
373
400
|
### fp analyze
|
|
374
401
|
|
|
375
402
|
Triage requirement quality: duplicates, fragments of a broader promise, vague wording,
|
|
@@ -459,6 +486,52 @@ classifications remain open for a human product decision. A stale candidate is r
|
|
|
459
486
|
later fingerprint always requires a new assessment. Assessing never changes the requirement
|
|
460
487
|
wording or CI outcome.
|
|
461
488
|
|
|
489
|
+
### fp spot-check
|
|
490
|
+
|
|
491
|
+
A random drug test for evidence. A green receipt proves a marked test ran and passed, not
|
|
492
|
+
that it checks what the requirement promises. `fp spot-check` picks one active requirement
|
|
493
|
+
that has evidence on record, at random, and prints its why and acceptance, its receipts, and
|
|
494
|
+
the source of every `fp:<slug>` test it finds in the checkout, then a brief.
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
fp spot-check # random pick; prints the seed
|
|
498
|
+
fp spot-check --seed 4821 # repeat that pick
|
|
499
|
+
fp spot-check --surface cli --json # narrow to one surface; machine-readable
|
|
500
|
+
fp spot-check print_container_qr print_label # chosen requirements, any status
|
|
501
|
+
fp spot-check --count 5 # five distinct requirements at random
|
|
502
|
+
fp spot-check --all # every active requirement with evidence
|
|
503
|
+
fp spot-check --test test/qr_test.rb # every marked test in the file
|
|
504
|
+
fp spot-check --test test/qr_test.rb:42 # only the test spanning that line
|
|
505
|
+
fp spot-check --path ../android # look for the tests in another checkout
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Use one way of choosing per run: slugs, `--count`, `--all`, or `--test` (which may be
|
|
509
|
+
repeated). The seed is printed only for the random picks. In `--json` the result is
|
|
510
|
+
`checks` (one entry per requirement, each with `requirement`, `evidence` and `tests`), plus
|
|
511
|
+
a single `brief` that applies to every check.
|
|
512
|
+
|
|
513
|
+
`--prompt` prints the whole spot check as instructions for an agent, so a person or a
|
|
514
|
+
schedule can hand it over as is (`fp spot-check --count 3 --prompt | claude -p`). It ends
|
|
515
|
+
with what to report for each requirement: `proves`, `does_not_prove` or `uncertain`, the
|
|
516
|
+
clauses no assertion checks, the probe made and the tests that failed, and a clean
|
|
517
|
+
`git status`. It can't be combined with `--json`.
|
|
518
|
+
|
|
519
|
+
`--jev` first sends each requirement's marked tests (up to 10) to Jev and adds its read:
|
|
520
|
+
`agree`, `gaps` or `uncertain`, how likely the tests are to cover the acceptance, the
|
|
521
|
+
likeliest gap (`unasserted_clause`, `asserts_other_behaviour`, `cannot_fail`,
|
|
522
|
+
`tests_a_double`), and a per-test likelihood. Treat it as a hint about where to aim the
|
|
523
|
+
probe. It doesn't replace the probe. Jev is for paid plans: a person on Pro or Lifetime,
|
|
524
|
+
or a project whose owner is (that is what lets an agent use it). Otherwise the spot check
|
|
525
|
+
prints a note and carries on without it. Nothing Jev reads is stored.
|
|
526
|
+
|
|
527
|
+
The brief: list each result and boundary the acceptance promises; name the clause each
|
|
528
|
+
test's assertions actually check; make one small change to the *product code* that breaks
|
|
529
|
+
the promise, run only those tests, and confirm one fails; revert and confirm `git status` is
|
|
530
|
+
clean. A test that stays green under the probe, or checks something the acceptance doesn't
|
|
531
|
+
say, doesn't prove the requirement. Report that to a human with what you tried. Don't
|
|
532
|
+
rewrite the test or the requirement as part of a spot check. The command itself is
|
|
533
|
+
read-only.
|
|
534
|
+
|
|
462
535
|
#### Upgrading an existing project
|
|
463
536
|
|
|
464
537
|
Changed-test auditing requires fp 0.0.17 or newer and the corresponding backend deployment.
|
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.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: net-http
|
|
@@ -103,6 +103,7 @@ files:
|
|
|
103
103
|
- lib/fp/commands/check.rb
|
|
104
104
|
- lib/fp/commands/ci_report.rb
|
|
105
105
|
- lib/fp/commands/comment.rb
|
|
106
|
+
- lib/fp/commands/comments.rb
|
|
106
107
|
- lib/fp/commands/config_cmd.rb
|
|
107
108
|
- lib/fp/commands/conversation.rb
|
|
108
109
|
- lib/fp/commands/export.rb
|
|
@@ -121,7 +122,9 @@ files:
|
|
|
121
122
|
- lib/fp/commands/show.rb
|
|
122
123
|
- lib/fp/commands/showroom.rb
|
|
123
124
|
- lib/fp/commands/showroom_media.rb
|
|
125
|
+
- lib/fp/commands/spot_check.rb
|
|
124
126
|
- lib/fp/commands/surfaces.rb
|
|
127
|
+
- lib/fp/commands/tutorial.rb
|
|
125
128
|
- lib/fp/commands/version.rb
|
|
126
129
|
- lib/fp/commands/whoami.rb
|
|
127
130
|
- lib/fp/config.rb
|
|
@@ -130,6 +133,12 @@ files:
|
|
|
130
133
|
- lib/fp/parity.rb
|
|
131
134
|
- lib/fp/project_config.rb
|
|
132
135
|
- lib/fp/source_fingerprint.rb
|
|
136
|
+
- lib/fp/tutorial/hexhaven/AGENTS.md
|
|
137
|
+
- lib/fp/tutorial/hexhaven/README.md
|
|
138
|
+
- lib/fp/tutorial/hexhaven/lib/hexhaven.rb
|
|
139
|
+
- lib/fp/tutorial/hexhaven/test/junit_reporter.rb
|
|
140
|
+
- lib/fp/tutorial/hexhaven/test/rules_test.rb
|
|
141
|
+
- lib/fp/tutorial/hexhaven/test/test_helper.rb
|
|
133
142
|
- lib/fp/version.rb
|
|
134
143
|
- skills/fp/SKILL.md
|
|
135
144
|
- skills/fp/references/cli.md
|