featureparity 0.0.25 → 0.0.27
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 +4 -1
- data/lib/fp/commands/ci_report.rb +1 -0
- data/lib/fp/commands/help.rb +20 -12
- data/lib/fp/commands/init.rb +26 -15
- data/lib/fp/commands/report.rb +112 -9
- data/lib/fp/commands/show.rb +9 -0
- data/lib/fp/version.rb +1 -1
- data/skills/fp/SKILL.md +27 -15
- 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: 4b09491a1b44b4ba693dfd567cdec5068d3410c577066435b541b2e9520ae04f
|
|
4
|
+
data.tar.gz: c9b7fb84bee3dbb3e8dfd8ee29f7c4d878747c0157efaa63fb020ead7934b2e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdf6cc31e230d20555285da1b44ee1816e94a88d3a5e3c02b22569cc63034d2f038d5183c12191f9ba446e5fdbc78a44ebe0b485e476f0dd5b9e021675338500
|
|
7
|
+
data.tar.gz: c9b9337383f9e7aecfa8caec130d99e3a1e93452481d14ffe1e0f95bd63c495480241d3b1578f0ff01ca5c7f81cc3b969a78cf232947a563cd307ade34b255c1
|
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, orphan_markers: nil)
|
|
284
|
+
test_revisions: nil, unmarked_failures: nil, orphan_markers: nil, sha: nil)
|
|
285
285
|
body = {
|
|
286
286
|
ci_url: ci_url,
|
|
287
287
|
surface: surface,
|
|
@@ -289,6 +289,9 @@ module Fp
|
|
|
289
289
|
}
|
|
290
290
|
# Only send pr_url when set, so a merged-branch run stays a plain ingest.
|
|
291
291
|
body[:pr_url] = pr_url if pr_url && !pr_url.empty?
|
|
292
|
+
# The checkout commit, so the receipt (or a PR run's branch verdict) links the
|
|
293
|
+
# exact revision the suite ran at.
|
|
294
|
+
body[:sha] = sha if sha && !sha.empty?
|
|
292
295
|
body[:results] = results if results && !results.empty?
|
|
293
296
|
# Testcase-level source receipts stay separate from aggregate Evidence outcomes:
|
|
294
297
|
# several tests may back one cell, and each can change independently.
|
|
@@ -146,6 +146,7 @@ module Fp
|
|
|
146
146
|
# with servers old enough to ignore client-resolved results.
|
|
147
147
|
junit_xml: resolved_reports.first[:xml],
|
|
148
148
|
pr_url: opts[:pr],
|
|
149
|
+
sha: commit_sha,
|
|
149
150
|
results: combined_results,
|
|
150
151
|
test_revisions: combined_revisions,
|
|
151
152
|
unmarked_failures: totals[:unmarked_failures],
|
data/lib/fp/commands/help.rb
CHANGED
|
@@ -289,24 +289,32 @@ module Fp
|
|
|
289
289
|
#
|
|
290
290
|
# Write the requirement as a product decision someone can approve, not a design
|
|
291
291
|
# task. It should sound like a capable teammate, not AI boilerplate.
|
|
292
|
-
# title: one product promise in present tense,
|
|
293
|
-
# why: one
|
|
294
|
-
# acceptance:
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
300
|
-
#
|
|
292
|
+
# title: one product promise in present tense, with a concrete subject and active verb
|
|
293
|
+
# why: one natural sentence naming the user need/problem and protected outcome
|
|
294
|
+
# acceptance: one independently verifiable rule of observable behaviour;
|
|
295
|
+
# append one concise concrete case as "Example: ..."
|
|
296
|
+
# Given/When/Then may clarify a rule, but is not mandatory. Split another result that
|
|
297
|
+
# could fail independently rather than comma-chaining it into the same requirement.
|
|
298
|
+
# Use the domain's real terms in examples and ask rather than invent consequential
|
|
299
|
+
# policy. Examples and tests ground and double-check the rule; they do not replace it.
|
|
300
|
+
# State each field directly. Avoid “This feature…”, “The goal is…”, “Users should be
|
|
301
|
+
# able to…”, “The system should…”, and task titles beginning “Enable” or “Implement”.
|
|
302
|
+
# Leave out components, data models, algorithms, endpoints, and UI controls unless
|
|
303
|
+
# they are themselves part of the user-visible contract.
|
|
304
|
+
# Include a price, quota, timeout, count, date, role, or fallback only when the source
|
|
305
|
+
# made it a real product decision; if important policy is missing, ask instead of
|
|
306
|
+
# inventing a plausible value.
|
|
301
307
|
#
|
|
302
308
|
# good title: "Dispatchers see current shipment status"
|
|
303
309
|
# good why: "Dispatchers need to spot delayed shipments before they miss a delivery window."
|
|
304
310
|
# good acceptance: "When status changes, the dashboard shows it; when unavailable, it says so instead of showing stale data as current."
|
|
305
311
|
# bad: "Implement Redis polling every 30 seconds to ensure a seamless experience."
|
|
306
312
|
# (invented policy, implementation detail, and filler; no product outcome)
|
|
307
|
-
# Before saving,
|
|
308
|
-
#
|
|
309
|
-
#
|
|
313
|
+
# Before saving, answer against the actual fields: who is this promise for, how would
|
|
314
|
+
# the concrete example prove it done, and why does the outcome matter? If an answer is
|
|
315
|
+
# vague, revise or ask. Then read it as a manager deciding scope: is the value clear,
|
|
316
|
+
# is the result observable, are decisions separated from guesses, and does every
|
|
317
|
+
# sentence earn its place? Do not save until the draft passes this review.
|
|
310
318
|
fp propose --project stowzilla \\
|
|
311
319
|
--slug print_container_qr \\
|
|
312
320
|
--name "Print QR code on container label" \\
|
data/lib/fp/commands/init.rb
CHANGED
|
@@ -470,18 +470,22 @@ module Fp
|
|
|
470
470
|
task.** It should read like a capable teammate wrote it: direct, specific, and free
|
|
471
471
|
of boilerplate such as “ensure a seamless experience.”
|
|
472
472
|
|
|
473
|
-
- **Title:** one clear product promise in present tense
|
|
474
|
-
capability, not the work (`“Dispatchers see
|
|
475
|
-
`“Implement status polling”`).
|
|
476
|
-
- **Why:** one
|
|
477
|
-
|
|
478
|
-
explain the mechanism.
|
|
479
|
-
- **Acceptance:**
|
|
480
|
-
the promise is true. State the trigger or situation, the result, and material
|
|
481
|
-
boundaries
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
473
|
+
- **Title:** one clear product promise in present tense, with a concrete subject and
|
|
474
|
+
active verb. Name the user-visible capability, not the work (`“Dispatchers see
|
|
475
|
+
current shipment status”`, not `“Implement status polling”`).
|
|
476
|
+
- **Why:** one natural sentence naming who needs this, the problem or risk, and the
|
|
477
|
+
outcome worth protecting. Do not repeat the title, narrate the request or bug
|
|
478
|
+
history, sell the feature, or explain the mechanism.
|
|
479
|
+
- **Acceptance:** one independently verifiable rule of observable behaviour that lets a
|
|
480
|
+
reviewer decide whether the promise is true. State the trigger or situation, the result, and material
|
|
481
|
+
boundaries that qualify that same promise. Given/When/Then may clarify those parts,
|
|
482
|
+
but it is not a mandatory template. Split a second result that could fail
|
|
483
|
+
independently into its own requirement instead of comma-chaining it here.
|
|
484
|
+
- **Example:** supply one concise concrete case that would pass the general rule, and
|
|
485
|
+
put it after the rule in Acceptance as `Example: ...`. Use the domain's real terms
|
|
486
|
+
and representative data, but ask rather than invent consequential policy. Do not mistake a few examples for the rule
|
|
487
|
+
or let marked tests stand for every case; neither replaces the general rule and
|
|
488
|
+
continued conversation.
|
|
485
489
|
- **Numbers and policy:** include a price, limit, timeout, date, role, or fallback only
|
|
486
490
|
when the source made it a real product decision. Never invent a plausible value to
|
|
487
491
|
fill a gap. If the decision matters and the source is silent, ask.
|
|
@@ -494,6 +498,10 @@ module Fp
|
|
|
494
498
|
unquestionable scope. Preserve important uncertainty as a question rather than
|
|
495
499
|
laundering a guess into acceptance criteria.
|
|
496
500
|
|
|
501
|
+
State the promise itself rather than commentary about it. Avoid openings such as “This
|
|
502
|
+
feature…”, “This requirement…”, “The goal is…”, “Users should be able to…”, and “The
|
|
503
|
+
system should…”. Use the real actor or product as the subject and an active verb.
|
|
504
|
+
|
|
497
505
|
- ✅ **Title:** `Dispatchers see current shipment status`
|
|
498
506
|
**Why:** `Dispatchers need to spot delayed shipments before they miss a delivery window.`
|
|
499
507
|
**Acceptance:** `When a shipment's status changes, the dashboard shows the new status; if status is unavailable, it says so instead of presenting stale data as current.`
|
|
@@ -502,9 +510,12 @@ module Fp
|
|
|
502
510
|
**Acceptance:** `Use a background worker and refresh every 30 seconds.`
|
|
503
511
|
This invents policy, prescribes a design, and never states the product outcome.
|
|
504
512
|
|
|
505
|
-
Before proposing,
|
|
506
|
-
|
|
507
|
-
|
|
513
|
+
Before proposing, answer three questions against the actual fields: Who is this promise
|
|
514
|
+
for? How would the concrete example prove it done? Why does the outcome matter? If any
|
|
515
|
+
answer is missing or vague, revise the draft or ask the user. Then read it as a manager deciding scope:
|
|
516
|
+
is the value clear, can the result be observed, are product decisions
|
|
517
|
+
distinguished from guesses, and does every sentence earn its place? If the draft fails
|
|
518
|
+
this review, revise it or ask before writing the record.
|
|
508
519
|
|
|
509
520
|
### 2. If the requirement is wrong, propose a change — don't silently edit it
|
|
510
521
|
|
data/lib/fp/commands/report.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'set'
|
|
3
5
|
require_relative 'attach'
|
|
4
6
|
|
|
5
7
|
module Fp
|
|
@@ -37,11 +39,14 @@ module Fp
|
|
|
37
39
|
help_flag :junit, 'JUnit XML report to batch-upload evidence from'
|
|
38
40
|
help_flag :base_dir, 'Where relative test-file paths in the report resolve from'
|
|
39
41
|
help_flag :attach_media, 'With --junit: upload the videos/screenshots the run recorded ([[ATTACHMENT|…]]) as visual evidence'
|
|
42
|
+
help_flag :stamp_all, 'With --junit: put --pr/--work-item on every receipt, not only on tests in files this branch changed'
|
|
40
43
|
help_example 'fp report print_container_qr --project stowzilla --surface api --file spec/qr_spec.rb --repo stowzilla/app'
|
|
41
44
|
help_example 'fp report --junit junit.xml --project stowzilla --surface api --repo stowzilla/app',
|
|
42
45
|
'Batch-upload from a JUnit report (slugs come from fp:<slug> markers)'
|
|
43
46
|
help_note "Agents report 'present' or 'stub' only. CI writes 'passing'/'failing' via fp ci-report.\n" \
|
|
44
|
-
|
|
47
|
+
"Always pass --test-type when a surface has more than one suite, or they overwrite each other.\n" \
|
|
48
|
+
'With --junit, --pr and --work-item land only on tests in files this branch changed (git); ' \
|
|
49
|
+
'the rest of the suite keeps the card and PR that introduced it.'
|
|
45
50
|
|
|
46
51
|
KNOWN_FLAGS = {
|
|
47
52
|
project: :string,
|
|
@@ -58,7 +63,8 @@ module Fp
|
|
|
58
63
|
state: :string,
|
|
59
64
|
junit: :string,
|
|
60
65
|
base_dir: :string,
|
|
61
|
-
attach_media: :boolean
|
|
66
|
+
attach_media: :boolean,
|
|
67
|
+
stamp_all: :boolean
|
|
62
68
|
}.freeze
|
|
63
69
|
|
|
64
70
|
VALID_STATES = %w[present stub].freeze
|
|
@@ -261,6 +267,12 @@ module Fp
|
|
|
261
267
|
|
|
262
268
|
project_id = resolve_project_id(opts[:project])
|
|
263
269
|
|
|
270
|
+
# --pr and --work-item say what prompted *this* work. A suite run covers every
|
|
271
|
+
# marker in the repo, so stamping them on every receipt rewrote the card and PR
|
|
272
|
+
# on requirements the branch never touched — "View Card" on an unrelated
|
|
273
|
+
# requirement led to whichever card last ran the suite (Fizzy #1631). Only the
|
|
274
|
+
# tests in files this branch changed get them; the rest keep their own.
|
|
275
|
+
provenance = provenance_scope(opts, base_dir)
|
|
264
276
|
reported = []
|
|
265
277
|
skipped_unknown = []
|
|
266
278
|
api_unavailable = false
|
|
@@ -289,9 +301,10 @@ module Fp
|
|
|
289
301
|
}
|
|
290
302
|
params[:test_type] = binding.test_type if binding.test_type
|
|
291
303
|
params[:line] = binding.line if binding.line
|
|
292
|
-
|
|
304
|
+
stamped = provenance.stamps?(binding.file)
|
|
305
|
+
params[:pr_url] = opts[:pr] if opts[:pr] && stamped
|
|
293
306
|
params[:sha] = opts[:sha] if opts[:sha]
|
|
294
|
-
params[:work_item_url] = opts[:work_item] if opts[:work_item]
|
|
307
|
+
params[:work_item_url] = opts[:work_item] if opts[:work_item] && stamped
|
|
295
308
|
params[:ci_url] = opts[:ci_url] if opts[:ci_url]
|
|
296
309
|
params[:title] = binding.title if binding.title
|
|
297
310
|
|
|
@@ -310,7 +323,7 @@ module Fp
|
|
|
310
323
|
exit 1
|
|
311
324
|
end
|
|
312
325
|
|
|
313
|
-
reported << { binding: binding, state: state, recorded: true,
|
|
326
|
+
reported << { binding: binding, state: state, recorded: true, stamped: stamped,
|
|
314
327
|
stored_status: stored_status_from(report_result),
|
|
315
328
|
stored_source: stored_source_from(report_result) }
|
|
316
329
|
end
|
|
@@ -321,7 +334,80 @@ module Fp
|
|
|
321
334
|
end
|
|
322
335
|
|
|
323
336
|
emit_junit_summary(opts, reported, skipped_unknown, unmatched, api_unavailable, collisions,
|
|
324
|
-
empty_name: empty_name, media: media, uploaded_media: uploaded_media
|
|
337
|
+
empty_name: empty_name, media: media, uploaded_media: uploaded_media,
|
|
338
|
+
provenance: provenance)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Which receipts of a --junit batch carry --pr/--work-item.
|
|
342
|
+
#
|
|
343
|
+
# `files` is the set of repo-relative paths this branch changed — the commits since
|
|
344
|
+
# it left the default branch, plus anything uncommitted or untracked — or, when
|
|
345
|
+
# the checkout *is* the default branch, the files in its HEAD commit. Nil means
|
|
346
|
+
# git couldn't say (not a repository, no default branch to compare with), and then
|
|
347
|
+
# every receipt is stamped as before, with a note. `--stamp-all` forces that.
|
|
348
|
+
ProvenanceScope = Struct.new(:mode, :files, keyword_init: true) do
|
|
349
|
+
def stamps?(file)
|
|
350
|
+
return true if mode == :all || files.nil?
|
|
351
|
+
|
|
352
|
+
files.include?(file.to_s)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def scoped?
|
|
356
|
+
mode != :all && !files.nil?
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def provenance_scope(opts, base_dir)
|
|
361
|
+
return ProvenanceScope.new(mode: :all, files: nil) if opts[:stamp_all]
|
|
362
|
+
return ProvenanceScope.new(mode: :all, files: nil) if opts[:pr].nil? && opts[:work_item].nil?
|
|
363
|
+
|
|
364
|
+
root = Fp::JUnit.repo_root_for(base_dir)
|
|
365
|
+
return ProvenanceScope.new(mode: :no_git, files: nil) if root.nil?
|
|
366
|
+
|
|
367
|
+
head = git_out(root, 'rev-parse', 'HEAD')
|
|
368
|
+
return ProvenanceScope.new(mode: :no_git, files: nil) if head.nil? || head.empty?
|
|
369
|
+
|
|
370
|
+
base = merge_base_with_default_branch(root)
|
|
371
|
+
committed =
|
|
372
|
+
if base && base != head
|
|
373
|
+
git_out(root, 'diff', '--name-only', base, 'HEAD')
|
|
374
|
+
elsif base
|
|
375
|
+
# On the default branch itself: the work is whatever this commit touched.
|
|
376
|
+
git_out(root, 'diff-tree', '--no-commit-id', '--name-only', '-r', '--root', 'HEAD')
|
|
377
|
+
end
|
|
378
|
+
return ProvenanceScope.new(mode: :no_git, files: nil) if committed.nil?
|
|
379
|
+
|
|
380
|
+
pending = git_out(root, 'diff', '--name-only', 'HEAD') || ''
|
|
381
|
+
untracked = git_out(root, 'ls-files', '--others', '--exclude-standard') || ''
|
|
382
|
+
files = [committed, pending, untracked].join("\n").split("\n").map(&:strip).reject(&:empty?).to_set
|
|
383
|
+
ProvenanceScope.new(mode: (base != head ? :branch : :commit), files: files)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# The merge base between HEAD and the default branch, trying origin's HEAD first
|
|
387
|
+
# and the usual names after it. Nil when none of them exists locally.
|
|
388
|
+
def merge_base_with_default_branch(root)
|
|
389
|
+
candidates = []
|
|
390
|
+
remote_head = git_out(root, 'symbolic-ref', '--short', '-q', 'refs/remotes/origin/HEAD')
|
|
391
|
+
candidates << remote_head if remote_head && !remote_head.empty?
|
|
392
|
+
candidates.concat(%w[origin/main origin/master main master])
|
|
393
|
+
candidates.uniq.each do |ref|
|
|
394
|
+
verified = git_out(root, 'rev-parse', '--verify', '-q', "#{ref}^{commit}")
|
|
395
|
+
next if verified.nil? || verified.empty?
|
|
396
|
+
|
|
397
|
+
base = git_out(root, 'merge-base', ref, 'HEAD')
|
|
398
|
+
return base if base && !base.empty?
|
|
399
|
+
end
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Stdout of a git command, stripped — '' when it printed nothing, nil on failure.
|
|
404
|
+
def git_out(root, *args)
|
|
405
|
+
stdout, _stderr, status = Open3.capture3('git', *args, chdir: root)
|
|
406
|
+
return nil unless status.success?
|
|
407
|
+
|
|
408
|
+
stdout.strip
|
|
409
|
+
rescue SystemCallError
|
|
410
|
+
nil
|
|
325
411
|
end
|
|
326
412
|
|
|
327
413
|
# Upload the media (videos/screenshots) a run recorded for its bound testcases as
|
|
@@ -397,10 +483,13 @@ module Fp
|
|
|
397
483
|
end
|
|
398
484
|
|
|
399
485
|
def emit_junit_summary(opts, reported, skipped_unknown, unmatched, api_unavailable, collisions = [],
|
|
400
|
-
empty_name: [], media: [], uploaded_media: [])
|
|
486
|
+
empty_name: [], media: [], uploaded_media: [], provenance: nil)
|
|
487
|
+
provenance ||= ProvenanceScope.new(mode: :all, files: nil)
|
|
488
|
+
stamping = !opts[:pr].nil? || !opts[:work_item].nil?
|
|
401
489
|
data = {
|
|
402
490
|
repo: opts[:repo],
|
|
403
|
-
reported: reported.map { |r| { slug: r[:binding].slug, surface: r[:binding].surface, test_type: r[:binding].test_type, file: r[:binding].file, line: r[:binding].line, state: r[:state], recorded: r[:recorded], stored_status: r[:stored_status], superseded_by_ci: ci_superseded?(r[:state], r[:stored_status], r[:stored_source]) } },
|
|
491
|
+
reported: reported.map { |r| { slug: r[:binding].slug, surface: r[:binding].surface, test_type: r[:binding].test_type, file: r[:binding].file, line: r[:binding].line, state: r[:state], recorded: r[:recorded], stamped: (stamping ? r[:stamped] != false : nil), stored_status: r[:stored_status], superseded_by_ci: ci_superseded?(r[:state], r[:stored_status], r[:stored_source]) }.compact },
|
|
492
|
+
provenance: (stamping ? { scoped: provenance.scoped?, mode: provenance.mode.to_s, stamped: reported.count { |r| r[:stamped] != false } } : nil),
|
|
404
493
|
skipped_unknown: skipped_unknown.map { |b| { slug: b.slug, surface: b.surface, file: b.file } },
|
|
405
494
|
unmatched_testcases: unmatched.map { |tc| { name: tc.name, file: tc.file } },
|
|
406
495
|
empty_name: empty_name,
|
|
@@ -423,7 +512,21 @@ module Fp
|
|
|
423
512
|
loc = r[:binding].line ? "#{r[:binding].file}:#{r[:binding].line}" : r[:binding].file
|
|
424
513
|
type = r[:binding].test_type ? " [#{r[:binding].test_type}]" : ''
|
|
425
514
|
shown_state = superseded ? "#{r[:stored_status]}, kept CI — #{r[:state]} ignored" : r[:state]
|
|
426
|
-
|
|
515
|
+
stamp = stamping && provenance.scoped? && r[:stamped] ? ' ← this change' : ''
|
|
516
|
+
puts " #{marker} #{r[:binding].slug} → #{r[:binding].surface}#{type} (#{shown_state}) #{loc}#{stamp}"
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
if stamping
|
|
520
|
+
puts
|
|
521
|
+
stamped = reported.count { |r| r[:stamped] != false }
|
|
522
|
+
what = [opts[:pr] && '--pr', opts[:work_item] && '--work-item'].compact.join(' and ')
|
|
523
|
+
if provenance.scoped?
|
|
524
|
+
where = provenance.mode == :commit ? 'the files in this commit' : 'the files this branch changed'
|
|
525
|
+
puts "ℹ️ #{what} recorded on #{stamped} of #{reported.length} receipt(s) — the tests in #{where}."
|
|
526
|
+
puts ' The rest keep the card and PR that introduced them. Pass --stamp-all to override.'
|
|
527
|
+
elsif provenance.mode == :no_git
|
|
528
|
+
puts "ℹ️ #{what} recorded on every receipt: git could not say which files this change touched."
|
|
529
|
+
end
|
|
427
530
|
end
|
|
428
531
|
|
|
429
532
|
emit_collision_warning(collisions)
|
data/lib/fp/commands/show.rb
CHANGED
|
@@ -164,6 +164,15 @@ module Fp
|
|
|
164
164
|
if e['merged_status'] && e['merged_status'] != e['status']
|
|
165
165
|
puts " Merged: #{e['merged_status']} (the default branch's last verdict)"
|
|
166
166
|
end
|
|
167
|
+
# An open pull request's CI has reported on this test since the default
|
|
168
|
+
# branch did. It never changes the status above (Fizzy #1631); it is
|
|
169
|
+
# the heads-up that a branch says otherwise, with the PR to look at.
|
|
170
|
+
if e['branch_status']
|
|
171
|
+
branch = ["On PR: #{e['branch_status']}"]
|
|
172
|
+
branch << e['branch_pr_url'] if e['branch_pr_url']
|
|
173
|
+
branch << '(not merged)'
|
|
174
|
+
puts " #{branch.join(' ')}"
|
|
175
|
+
end
|
|
167
176
|
puts " Type: #{e['test_type']}" if e['test_type'] && e['test_type'] != 'unspecified'
|
|
168
177
|
if e['source_file']
|
|
169
178
|
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
|
@@ -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
|
|
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.27
|
|
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-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: net-http
|