featureparity 0.0.27 → 0.0.29

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fp/cli.rb +14 -2
  3. data/lib/fp/client.rb +79 -2
  4. data/lib/fp/commands/analyze.rb +17 -0
  5. data/lib/fp/commands/base.rb +16 -0
  6. data/lib/fp/commands/check.rb +38 -4
  7. data/lib/fp/commands/ci_report.rb +2 -2
  8. data/lib/fp/commands/comment.rb +1 -1
  9. data/lib/fp/commands/comments.rb +146 -0
  10. data/lib/fp/commands/coverage.rb +149 -0
  11. data/lib/fp/commands/doctor.rb +228 -0
  12. data/lib/fp/commands/help.rb +41 -1
  13. data/lib/fp/commands/history.rb +16 -1
  14. data/lib/fp/commands/init.rb +181 -117
  15. data/lib/fp/commands/list.rb +58 -4
  16. data/lib/fp/commands/mark.rb +167 -0
  17. data/lib/fp/commands/projects.rb +6 -1
  18. data/lib/fp/commands/propose.rb +435 -53
  19. data/lib/fp/commands/propose_change.rb +13 -7
  20. data/lib/fp/commands/question.rb +137 -0
  21. data/lib/fp/commands/report.rb +40 -4
  22. data/lib/fp/commands/run.rb +147 -0
  23. data/lib/fp/commands/show.rb +41 -3
  24. data/lib/fp/commands/spot_check.rb +458 -0
  25. data/lib/fp/commands/tutorial.rb +95 -0
  26. data/lib/fp/commands/whoami.rb +2 -0
  27. data/lib/fp/commands.rb +8 -0
  28. data/lib/fp/config.rb +12 -0
  29. data/lib/fp/junit.rb +74 -11
  30. data/lib/fp/repo_detector.rb +488 -0
  31. data/lib/fp/requirement_lint.rb +105 -0
  32. data/lib/fp/rspec_json.rb +54 -0
  33. data/lib/fp/tutorial/hexhaven/AGENTS.md +5 -0
  34. data/lib/fp/tutorial/hexhaven/README.md +29 -0
  35. data/lib/fp/tutorial/hexhaven/lib/hexhaven.rb +36 -0
  36. data/lib/fp/tutorial/hexhaven/test/junit_reporter.rb +134 -0
  37. data/lib/fp/tutorial/hexhaven/test/rules_test.rb +36 -0
  38. data/lib/fp/tutorial/hexhaven/test/test_helper.rb +6 -0
  39. data/lib/fp/version.rb +1 -1
  40. data/lib/fp.rb +1 -0
  41. data/skills/fp/SKILL.md +244 -105
  42. data/skills/fp/references/cli.md +304 -3
  43. metadata +19 -2
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fp
4
+ module Commands
5
+ # fp mark <slug> <file>:<line> [--surface a,b] [--type e2e] [--comment '#'] [--dry-run]
6
+ #
7
+ # Insert an fp:<slug> marker directly above the test at <file>:<line>, in that
8
+ # file's own comment syntax and indentation (Fizzy #1786). Marking by hand across
9
+ # Ruby, JS, Swift and SQL meant a script per language and the occasional marker the
10
+ # scanner didn't recognise; this writes one the scanner reads back, and says which
11
+ # test it binds to. Purely local: no API key, no network.
12
+ class Mark < Base
13
+ help_summary 'Insert an fp:<slug> marker above a test, in the file\'s own comment syntax'
14
+ help_usage 'fp mark <slug> <file>:<line> [--surface a,b] [--type <test-type>] [--dry-run]'
15
+ help_flag :surface, 'Pin the marker to these surfaces (fp:slug@a,b); omit to use --surface at report time'
16
+ help_flag :type, 'Pin the kind of test (fp:slug@surface:e2e); needs --surface'
17
+ help_flag :comment, "Comment prefix to use when the extension isn't known (e.g. '#', '//', '--')"
18
+ help_flag :dry_run, 'Show the marker and where it would go; change nothing'
19
+ help_example 'fp mark academic_experience_required_fields spec/models/academic_experience_spec.rb:52'
20
+ help_example 'fp mark checkout_flow e2e/checkout.spec.ts:10 --surface web --type e2e'
21
+ help_note "<line> is the test's own line (the `it`/`test`/`def test_` line); the marker goes directly above it.\n" \
22
+ 'Marking a test binds it to a requirement: never mark a test that is currently failing to make it count.'
23
+
24
+ KNOWN_FLAGS = {
25
+ surface: :string,
26
+ type: :string,
27
+ comment: :string,
28
+ dry_run: :boolean
29
+ }.freeze
30
+
31
+ COMMENT_PREFIXES = {
32
+ '#' => %w[rb rake gemspec ru py sh bash zsh yml yaml toml ex exs r pl cr jl nim tf],
33
+ '//' => %w[js jsx mjs cjs ts tsx mts cts swift kt kts java go rs c cc cpp cxx h hpp m mm cs dart
34
+ scala php groovy gradle zig v],
35
+ '--' => %w[sql lua hs elm]
36
+ }.freeze
37
+
38
+ # Files named without an extension that are Ruby.
39
+ RUBY_FILENAMES = %w[Rakefile Gemfile Guardfile Podfile Fastfile].freeze
40
+
41
+ SLUG_RE = /\A[a-z0-9][a-z0-9_-]*\z/.freeze
42
+ KEY_RE = /\A[a-z0-9_-]+\z/.freeze
43
+
44
+ def run(args)
45
+ opts, positional = parse_flags(args, KNOWN_FLAGS)
46
+ slug, location = positional
47
+
48
+ unless slug && location
49
+ output.error('Usage: fp mark <slug> <file>:<line>')
50
+ exit 1
51
+ end
52
+
53
+ fail_with("'#{slug}' isn't a valid slug (lowercase letters, numbers, - and _).") unless slug.match?(SLUG_RE)
54
+
55
+ path, line_no = parse_location(location)
56
+ surfaces = (opts[:surface] || '').split(',').map(&:strip).reject(&:empty?)
57
+ bad = surfaces.reject { |s| s.match?(KEY_RE) }
58
+ fail_with("Invalid surface key(s): #{bad.join(', ')}") unless bad.empty?
59
+ if opts[:type]
60
+ fail_with("Invalid test type '#{opts[:type]}'") unless opts[:type].match?(KEY_RE)
61
+ fail_with('--type needs --surface: a marker pins its kind after its surfaces (fp:slug@api:e2e)') if surfaces.empty?
62
+ end
63
+
64
+ prefix = opts[:comment] || comment_prefix_for(path)
65
+ unless prefix
66
+ fail_with("Don't know the comment syntax for #{File.basename(path)}. Pass --comment, e.g. --comment '#' or --comment '//'.")
67
+ end
68
+
69
+ lines = File.read(path).lines
70
+ if line_no > lines.length
71
+ fail_with("#{path} has #{lines.length} line(s); there is no line #{line_no}.")
72
+ end
73
+
74
+ marker = "fp:#{slug}"
75
+ marker += "@#{surfaces.join(',')}" unless surfaces.empty?
76
+ marker += ":#{opts[:type]}" if opts[:type]
77
+
78
+ target = lines[line_no - 1]
79
+ indent = target[/\A[ \t]*/]
80
+ comment_line = "#{indent}#{prefix} #{marker}#{line_ending(target)}"
81
+
82
+ if already_marked?(lines, line_no, marker)
83
+ return report(path, line_no, marker, nil, already: true)
84
+ end
85
+
86
+ updated = lines.dup.insert(line_no - 1, comment_line)
87
+ found = Fp::JUnit.scan_markers(updated.join).find { |m| m[:line] == line_no && m[:slug] == slug }
88
+ unless found
89
+ fail_with("A '#{prefix}' comment isn't one fp's scanner reads as a marker. Use #, //, -- or /* … */.")
90
+ end
91
+
92
+ File.write(path, updated.join) unless opts[:dry_run]
93
+ report(path, line_no, marker, found, dry_run: opts[:dry_run], comment_line: comment_line.chomp)
94
+ end
95
+
96
+ private
97
+
98
+ def parse_location(location)
99
+ m = location.match(/\A(.+):(\d+)\z/)
100
+ fail_with("Give the test's location as <file>:<line> (got '#{location}').") unless m
101
+
102
+ path = m[1]
103
+ line_no = m[2].to_i
104
+ fail_with("No such file: #{path}") unless File.file?(path)
105
+ fail_with('Line numbers start at 1.') if line_no < 1
106
+
107
+ [path, line_no]
108
+ end
109
+
110
+ def comment_prefix_for(path)
111
+ base = File.basename(path)
112
+ return '#' if RUBY_FILENAMES.include?(base)
113
+
114
+ ext = File.extname(base).delete('.').downcase
115
+ COMMENT_PREFIXES.find { |_, exts| exts.include?(ext) }&.first
116
+ end
117
+
118
+ def line_ending(line)
119
+ line.end_with?("\r\n") ? "\r\n" : "\n"
120
+ end
121
+
122
+ # The same marker already sits in the comment block directly above the test.
123
+ def already_marked?(lines, line_no, marker)
124
+ i = line_no - 2
125
+ while i >= 0 && lines[i].match?(Fp::JUnit::COMMENT_LINE_RE)
126
+ return true if lines[i].scan(Fp::JUnit::MARKER_RE).any? { |m| marker_string(m) == marker }
127
+
128
+ i -= 1
129
+ end
130
+ false
131
+ end
132
+
133
+ def marker_string((slug, surfaces, type))
134
+ s = "fp:#{slug}"
135
+ s += "@#{surfaces}" if surfaces
136
+ s += ":#{type}" if type
137
+ s
138
+ end
139
+
140
+ def report(path, line_no, marker, found, already: false, dry_run: false, comment_line: nil)
141
+ data = { file: path, line: line_no, marker: marker, already_marked: already, dry_run: dry_run,
142
+ binds_to: found && found[:title] }.compact
143
+ output.success(data) do
144
+ if already
145
+ puts "#{path}:#{line_no} is already marked #{marker}; nothing changed."
146
+ next
147
+ end
148
+
149
+ verb = dry_run ? 'Would insert' : 'Inserted'
150
+ puts "#{verb} at #{path}:#{line_no}:"
151
+ puts " #{comment_line.strip}"
152
+ if found[:title]
153
+ puts "It binds to: #{found[:title]}"
154
+ else
155
+ puts "⚠️ No test declaration was recognised below line #{line_no}. Check that the line is the test itself;"
156
+ puts ' if the runner reports file paths, the marker still binds by file.'
157
+ end
158
+ end
159
+ end
160
+
161
+ def fail_with(message)
162
+ output.error(message)
163
+ exit 1
164
+ end
165
+ end
166
+ end
167
+ end
@@ -87,7 +87,11 @@ module Fp
87
87
  output.success(result[:data]) do
88
88
  if projects.empty?
89
89
  puts 'No projects found.'
90
- puts 'Create one with: fp projects create <slug> --name "<Name>"'
90
+ if (hint = config&.environment_mismatch_hint)
91
+ puts hint
92
+ else
93
+ puts 'Create one with: fp projects create <slug> --name "<Name>"'
94
+ end
91
95
  else
92
96
  headers = %w[SLUG NAME SURFACES]
93
97
  rows = projects.map do |w|
@@ -132,6 +136,7 @@ module Fp
132
136
  surfaces = split_list(opts[:surfaces])
133
137
  params[:surfaces] = surfaces unless surfaces.empty?
134
138
 
139
+ warn_if_not_production!
135
140
  result = client.create_project(params)
136
141
 
137
142
  unless result[:ok]