featureparity 0.0.27 → 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 +28 -0
- data/lib/fp/commands/history.rb +12 -1
- data/lib/fp/commands/init.rb +9 -0
- 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/references/cli.md +73 -0
- metadata +11 -2
|
@@ -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/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-
|
|
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
|