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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7a4558cdeaa90d0b4197daca351b9eadbeee5af21cb8454e9a1f44b3725bbaa
4
- data.tar.gz: d0134aeca43dd6ee3415bfafe25ea7f67212eda1b8577a3bdfd9d9a3800aa150
3
+ metadata.gz: 4b09491a1b44b4ba693dfd567cdec5068d3410c577066435b541b2e9520ae04f
4
+ data.tar.gz: c9b7fb84bee3dbb3e8dfd8ee29f7c4d878747c0157efaa63fb020ead7934b2e7
5
5
  SHA512:
6
- metadata.gz: 70827aadfd85037c992e670923ae4c7065c6ba913905028b903f4b6535f8498f530d3ec51a9ff245aa9734a3f044d6ca779b5e8b849b0a15d1b6b2aaed360e2f
7
- data.tar.gz: 9ba46d5f8efa5a57e4b23c2557969cdd5c6b6697cb8584bb98b7c11fc237a099743c33474b46d05444361785278707981206af09e7c32570261ae5b104a9885d
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],
@@ -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, not the implementation project
293
- # why: one or two natural sentences naming the user need/problem and outcome
294
- # acceptance: trigger or situation + observable result + material boundaries
295
- # Use examples and tests to ground and double-check the general rule, not to replace
296
- # it or ongoing conversation. Leave out components, data models, algorithms, endpoints,
297
- # and UI controls unless they are themselves part of the user-visible contract.
298
- # Include a price, quota, timeout, count, date,
299
- # role, or fallback only when the source made it a real product decision; if an
300
- # important policy is missing, ask instead of inventing a plausible value.
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, read it as a manager deciding scope: is the value clear, is the
308
- # result observable, are decisions separated from guesses, and does every sentence
309
- # earn its place?
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" \\
@@ -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. Name the user-visible
474
- capability, not the work (`“Dispatchers see current shipment status”`, not
475
- `“Implement status polling”`).
476
- - **Why:** one or two natural sentences naming who needs this, the problem or risk,
477
- and the outcome worth protecting. Do not repeat the title, sell the feature, or
478
- explain the mechanism.
479
- - **Acceptance:** the observable behaviour that would let a reviewer decide whether
480
- the promise is true. State the trigger or situation, the result, and material
481
- boundaries such as permissions, failure states, and affected surfaces. Use examples
482
- and marked tests to ground and double-check the rule. Do not mistake a few examples for the rule
483
- or let tests stand for every case; neither replaces the general rule and continued
484
- conversation.
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, read the draft once as a manager deciding scope: is the value clear,
506
- can the result be observed, are product decisions distinguished from guesses, and can
507
- every sentence earn its place? If not, revise or ask before writing the record.
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
 
@@ -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
- 'Always pass --test-type when a surface has more than one suite, or they overwrite each other.'
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
- params[:pr_url] = opts[:pr] if opts[:pr]
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
- puts " #{marker} #{r[:binding].slug} → #{r[:binding].surface}#{type} (#{shown_state}) #{loc}"
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fp
4
- VERSION = '0.0.25'
4
+ VERSION = '0.0.27'
5
5
  end
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. Name the capability or outcome, not
206
- the project (`Dispatchers see current shipment status`, not `Implement status polling`).
207
- - **Why:** one or two natural sentences saying who needs this, the problem or risk, and the
208
- outcome worth protecting. Do not repeat the title, advertise the feature, or explain how
209
- it will be built. Delete AI filler such as “ensure a seamless, robust experience.”
210
- - **Acceptance:** observable behaviour that lets someone decide whether the promise is true.
211
- State the relevant trigger or situation, the result, and material boundaries. Include
212
- failure behaviour, permissions, or cross-surface consistency when those affect the promise.
213
- - **Examples:** use concrete examples or Given/When/Then cases when they clarify a rule or an
214
- edge. Examples and executable tests ground and double-check the specification; they do not
215
- define every case or replace the general rule and continued conversation.
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, read the draft as a manager deciding scope: Is the value clear? Can the result
234
- be observed? Are real product decisions distinguished from guesses? Could every clause fail
235
- independently and, if so, should it be split? Does every sentence earn its place? Revise or ask
236
- before creating the record if the answer is no.
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.25
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-10-23 00:00:00.000000000 Z
11
+ date: 2026-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-http