carson 3.3.0 → 3.5.0

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: 91c767b37c33c1d69a377f509b5503ef342fef4ff4f3629439e998938d99b24b
4
- data.tar.gz: 2b615ad6df7610917d400166a5d735c9bc3f7e51b52eeda33d48446be9916d9c
3
+ metadata.gz: e8fc2b54698568aaec7d7f4ec39ecd32f56f44a395555a34099b24ec52a1d608
4
+ data.tar.gz: e3b0a5a14c733d6b2e9f5c08475d89a53f0a3d1faf8a2fb85ed72bcb76f662ef
5
5
  SHA512:
6
- metadata.gz: df671d4ed658b1aa98290cfcdbc5cd47c78c702d83254bdc4e79d9ed5755320b3cd9c08f664e20633830661506fe8a0cdcb0f8c03e1314623a6074a444f7d23d
7
- data.tar.gz: e05a41a7fb961deac1f63eccb77b8d03eb7d694a557baebaead88f5f38d9bab91a1b4643f71b4bf09741e51946ab2c6707a288e7a22fe2ea981ee5769e370b2f
6
+ metadata.gz: 9d3733f98a23dffdc69c1ebde71e94595c22e662f5ead86b97988fa3f5cf0e8daf56806deb82ceb1d034e0913640d9a8b7cafc26992f94e43dd96a3e304d6812
7
+ data.tar.gz: eb44eca84a943a461668b478e2bed295b3a73ee7e916cc5d85a837c52f5be09326b369e07a99812d4ffdc883914511d18c7f0489e151f3d4d66afe8ddaf97402
data/RELEASE.md CHANGED
@@ -5,6 +5,39 @@ Release-note scope rule:
5
5
  - `RELEASE.md` records only version deltas, breaking changes, and migration actions.
6
6
  - Operational usage guides live in `MANUAL.md` and `API.md`.
7
7
 
8
+ ## 3.5.0 — Sync JSON + Recovery
9
+
10
+ ### What changed
11
+
12
+ - **`carson sync --json`** — machine-readable JSON output for the sync command. The JSON envelope includes `command`, `status`, `ahead`, `behind`, `main_branch`, `remote`, `exit_code`, and on failure: `error` and `recovery`.
13
+ - **Recovery-aware sync errors** — dirty working tree now includes a recovery command in both human (`Recovery: ...`) and JSON output.
14
+
15
+ ### UX
16
+
17
+ - JSON output suppresses git command output (fetch, pull) to keep the JSON envelope clean.
18
+ - Human output remains unchanged when `--json` is not passed.
19
+
20
+ ### Migration
21
+
22
+ - No breaking changes. `carson sync` without `--json` behaves identically to 3.4.0.
23
+
24
+ ## 3.4.0 — Audit JSON
25
+
26
+ ### What changed
27
+
28
+ - **`carson audit --json`** — machine-readable JSON output for the audit command. The JSON envelope includes `command`, `status`, `branch`, `hooks`, `main_sync`, `pr`, `checks`, `baseline`, `problems`, and `exit_code`. Agents can parse audit results programmatically instead of regex-matching human-readable text.
29
+
30
+ ### UX
31
+
32
+ - JSON output is only produced when `--json` is passed; default human-readable output is unchanged.
33
+ - The `problems` array contains the same concise problem strings shown in non-verbose human output, making it easy for agents to surface actionable issues.
34
+ - `hooks.status` is `"ok"` or `"mismatch"`, `main_sync.status` is `"ok"`, `"ahead"`, `"behind"`, or `"unknown"`.
35
+
36
+ ### Migration
37
+
38
+ - No breaking changes. `carson audit` without `--json` behaves identically to 3.3.0.
39
+ - The `audit!` method now accepts `json_output:` keyword argument (default `false`).
40
+
8
41
  ## 3.3.0 — Deliver Refinements
9
42
 
10
43
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.3.0
1
+ 3.5.0
data/lib/carson/cli.rb CHANGED
@@ -53,7 +53,7 @@ module Carson
53
53
 
54
54
  def self.build_parser
55
55
  OptionParser.new do |opts|
56
- opts.banner = "Usage: carson [status [--json]|setup|audit|sync|deliver [--merge] [--json] [--title T] [--body-file F]|prune [--all]|worktree create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
56
+ opts.banner = "Usage: carson [status [--json]|setup|audit [--json]|sync [--json]|deliver [--merge] [--json] [--title T] [--body-file F]|prune [--all]|worktree create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
57
57
  end
58
58
  end
59
59
 
@@ -88,6 +88,10 @@ module Carson
88
88
  parse_worktree_subcommand( argv: argv, parser: parser, err: err )
89
89
  when "review"
90
90
  parse_named_subcommand( command: command, usage: "gate|sweep", argv: argv, parser: parser, err: err )
91
+ when "audit"
92
+ parse_audit_command( argv: argv, err: err )
93
+ when "sync"
94
+ parse_sync_command( argv: argv, err: err )
91
95
  when "status"
92
96
  parse_status_command( argv: argv, err: err )
93
97
  when "deliver"
@@ -242,6 +246,24 @@ module Carson
242
246
  { command: :invalid }
243
247
  end
244
248
 
249
+ def self.parse_audit_command( argv:, err: )
250
+ json_flag = argv.delete( "--json" ) ? true : false
251
+ unless argv.empty?
252
+ err.puts "#{BADGE} Unexpected arguments for audit: #{argv.join( ' ' )}"
253
+ return { command: :invalid }
254
+ end
255
+ { command: "audit", json: json_flag }
256
+ end
257
+
258
+ def self.parse_sync_command( argv:, err: )
259
+ json_flag = argv.delete( "--json" ) ? true : false
260
+ unless argv.empty?
261
+ err.puts "#{BADGE} Unexpected arguments for sync: #{argv.join( ' ' )}"
262
+ return { command: :invalid }
263
+ end
264
+ { command: "sync", json: json_flag }
265
+ end
266
+
245
267
  def self.parse_status_command( argv:, err: )
246
268
  json_flag = argv.delete( "--json" ) ? true : false
247
269
  unless argv.empty?
@@ -321,9 +343,9 @@ module Carson
321
343
  when "setup"
322
344
  runtime.setup!( cli_choices: parsed.fetch( :cli_choices, {} ) )
323
345
  when "audit"
324
- runtime.audit!
346
+ runtime.audit!( json_output: parsed.fetch( :json, false ) )
325
347
  when "sync"
326
- runtime.sync!
348
+ runtime.sync!( json_output: parsed.fetch( :json, false ) )
327
349
  when "prune"
328
350
  runtime.prune!
329
351
  when "prune:all"
@@ -1,13 +1,20 @@
1
+ # Pre-commit audit — checks hooks, main sync, PR checks, and CI baseline.
2
+ # Exits with EXIT_BLOCK when policy violations are found.
3
+ # Supports --json for machine-readable structured output.
1
4
  require "cgi"
2
5
 
3
6
  module Carson
4
7
  class Runtime
5
8
  module Audit
6
- def audit!
9
+ def audit!( json_output: false )
7
10
  fingerprint_status = block_if_outsider_fingerprints!
8
11
  return fingerprint_status unless fingerprint_status.nil?
9
12
  unless head_exists?
10
- puts_line "No commits yet — audit skipped for initial commit."
13
+ if json_output
14
+ out.puts JSON.pretty_generate( { command: "audit", status: "skipped", reason: "no commits yet", exit_code: EXIT_OK } )
15
+ else
16
+ puts_line "No commits yet — audit skipped for initial commit."
17
+ end
11
18
  return EXIT_OK
12
19
  end
13
20
  audit_state = "ok"
@@ -22,6 +29,7 @@ module Carson
22
29
  puts_verbose ""
23
30
  puts_verbose "[Hooks]"
24
31
  hooks_ok = hooks_health_report
32
+ hooks_status = hooks_ok ? "ok" : "mismatch"
25
33
  unless hooks_ok
26
34
  audit_state = "block"
27
35
  audit_concise_problems << "Hooks: mismatch — run carson refresh."
@@ -29,23 +37,27 @@ module Carson
29
37
  puts_verbose ""
30
38
  puts_verbose "[Main Sync Status]"
31
39
  ahead_count, behind_count, main_error = main_sync_counts
40
+ main_sync = { ahead: 0, behind: 0, status: "ok" }
32
41
  if main_error
33
42
  puts_verbose "main_vs_remote_main: unknown"
34
43
  puts_verbose "WARN: unable to calculate main sync status (#{main_error})."
35
44
  audit_state = "attention" if audit_state == "ok"
36
45
  audit_concise_problems << "Main sync: unable to determine — check remote connectivity."
46
+ main_sync = { ahead: 0, behind: 0, status: "unknown", error: main_error }
37
47
  elsif ahead_count.positive?
38
48
  puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
39
49
  puts_verbose "main_vs_remote_main_behind: #{behind_count}"
40
50
  puts_verbose "ACTION: local #{config.main_branch} is ahead of #{config.git_remote}/#{config.main_branch} by #{ahead_count} commit#{plural_suffix( count: ahead_count )}; reset local drift before commit/push workflows."
41
51
  audit_state = "block"
42
52
  audit_concise_problems << "Main sync (#{config.git_remote}): ahead by #{ahead_count} — git fetch #{config.git_remote}, or carson setup to switch remote."
53
+ main_sync = { ahead: ahead_count, behind: behind_count, status: "ahead" }
43
54
  elsif behind_count.positive?
44
55
  puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
45
56
  puts_verbose "main_vs_remote_main_behind: #{behind_count}"
46
57
  puts_verbose "ACTION: local #{config.main_branch} is behind #{config.git_remote}/#{config.main_branch} by #{behind_count} commit#{plural_suffix( count: behind_count )}; run carson sync."
47
58
  audit_state = "attention" if audit_state == "ok"
48
59
  audit_concise_problems << "Main sync (#{config.git_remote}): behind by #{behind_count} — run carson sync."
60
+ main_sync = { ahead: ahead_count, behind: behind_count, status: "behind" }
49
61
  else
50
62
  puts_verbose "main_vs_remote_main_ahead: 0"
51
63
  puts_verbose "main_vs_remote_main_behind: 0"
@@ -109,15 +121,40 @@ module Carson
109
121
  audit_status: audit_state
110
122
  )
111
123
  )
112
- puts_verbose ""
113
- puts_verbose "[Audit Result]"
114
- puts_verbose "status: #{audit_state}"
115
- puts_verbose( audit_state == "block" ? "ACTION: local policy block must be resolved before commit/push." : "ACTION: no local hard block detected." )
116
- unless verbose?
117
- audit_concise_problems.each { |problem| puts_line problem }
118
- puts_line "Audit: #{audit_state}"
124
+ exit_code = audit_state == "block" ? EXIT_BLOCK : EXIT_OK
125
+
126
+ if json_output
127
+ result = {
128
+ command: "audit",
129
+ status: audit_state,
130
+ branch: current_branch,
131
+ hooks: { status: hooks_status },
132
+ main_sync: main_sync,
133
+ pr: monitor_report[ :pr ],
134
+ checks: monitor_report.fetch( :checks ),
135
+ baseline: {
136
+ status: default_branch_baseline.fetch( :status ),
137
+ repository: default_branch_baseline[ :repository ],
138
+ failing_count: default_branch_baseline.fetch( :failing_count ),
139
+ pending_count: default_branch_baseline.fetch( :pending_count ),
140
+ advisory_failing_count: default_branch_baseline.fetch( :advisory_failing_count ),
141
+ advisory_pending_count: default_branch_baseline.fetch( :advisory_pending_count )
142
+ },
143
+ problems: audit_concise_problems,
144
+ exit_code: exit_code
145
+ }
146
+ out.puts JSON.pretty_generate( result )
147
+ else
148
+ puts_verbose ""
149
+ puts_verbose "[Audit Result]"
150
+ puts_verbose "status: #{audit_state}"
151
+ puts_verbose( audit_state == "block" ? "ACTION: local policy block must be resolved before commit/push." : "ACTION: no local hard block detected." )
152
+ unless verbose?
153
+ audit_concise_problems.each { |problem| puts_line problem }
154
+ puts_line "Audit: #{audit_state}"
155
+ end
119
156
  end
120
- audit_state == "block" ? EXIT_BLOCK : EXIT_OK
157
+ exit_code
121
158
  end
122
159
 
123
160
  private
@@ -1,39 +1,83 @@
1
+ # Syncs local main branch with remote main.
2
+ # Supports --json for machine-readable structured output.
1
3
  module Carson
2
4
  class Runtime
3
5
  module Local
4
- def sync!
6
+ def sync!( json_output: false )
5
7
  fingerprint_status = block_if_outsider_fingerprints!
6
8
  return fingerprint_status unless fingerprint_status.nil?
7
9
 
8
10
  unless working_tree_clean?
9
- puts_line "BLOCK: working tree is dirty; commit/stash first, then run carson sync."
10
- return EXIT_BLOCK
11
+ return sync_finish(
12
+ result: { command: "sync", status: "block", error: "working tree is dirty", recovery: "git add -A && git commit, then carson sync" },
13
+ exit_code: EXIT_BLOCK, json_output: json_output
14
+ )
11
15
  end
12
16
  start_branch = current_branch
13
17
  switched = false
14
- git_system!( "fetch", config.git_remote, "--prune" )
18
+ sync_git!( "fetch", config.git_remote, "--prune", json_output: json_output )
15
19
  if start_branch != config.main_branch
16
- git_system!( "switch", config.main_branch )
20
+ sync_git!( "switch", config.main_branch, json_output: json_output )
17
21
  switched = true
18
22
  end
19
- git_system!( "pull", "--ff-only", config.git_remote, config.main_branch )
23
+ sync_git!( "pull", "--ff-only", config.git_remote, config.main_branch, json_output: json_output )
20
24
  ahead_count, behind_count, error_text = main_sync_counts
21
25
  if error_text
22
- puts_line "BLOCK: unable to verify main sync state (#{error_text})."
23
- return EXIT_BLOCK
26
+ return sync_finish(
27
+ result: { command: "sync", status: "block", error: "unable to verify main sync state (#{error_text})" },
28
+ exit_code: EXIT_BLOCK, json_output: json_output
29
+ )
24
30
  end
25
31
  if ahead_count.zero? && behind_count.zero?
26
- puts_line "OK: local #{config.main_branch} is now in sync with #{config.git_remote}/#{config.main_branch}."
27
- return EXIT_OK
32
+ return sync_finish(
33
+ result: { command: "sync", status: "ok", ahead: 0, behind: 0, main_branch: config.main_branch, remote: config.git_remote },
34
+ exit_code: EXIT_OK, json_output: json_output
35
+ )
28
36
  end
29
- puts_line "BLOCK: local #{config.main_branch} still diverges (ahead=#{ahead_count}, behind=#{behind_count})."
30
- EXIT_BLOCK
37
+ sync_finish(
38
+ result: { command: "sync", status: "block", ahead: ahead_count, behind: behind_count, main_branch: config.main_branch, remote: config.git_remote, error: "local #{config.main_branch} still diverges" },
39
+ exit_code: EXIT_BLOCK, json_output: json_output
40
+ )
31
41
  ensure
32
42
  git_system!( "switch", start_branch ) if switched && branch_exists?( branch_name: start_branch )
33
43
  end
34
44
 
35
45
  private
36
46
 
47
+ # Runs a git command, suppressing stdout/stderr in JSON mode to keep output clean.
48
+ def sync_git!( *args, json_output: false )
49
+ if json_output
50
+ _, stderr_text, success, = git_run( *args )
51
+ raise "git #{args.join( ' ' )} failed: #{stderr_text.to_s.strip}" unless success
52
+ else
53
+ git_system!( *args )
54
+ end
55
+ end
56
+
57
+ # Unified output for sync results — JSON or human-readable.
58
+ def sync_finish( result:, exit_code:, json_output: )
59
+ result[ :exit_code ] = exit_code
60
+
61
+ if json_output
62
+ out.puts JSON.pretty_generate( result )
63
+ else
64
+ print_sync_human( result: result )
65
+ end
66
+
67
+ exit_code
68
+ end
69
+
70
+ # Human-readable output for sync results.
71
+ def print_sync_human( result: )
72
+ if result[ :error ]
73
+ puts_line "BLOCK: #{result[ :error ]}."
74
+ puts_line " Recovery: #{result[ :recovery ]}" if result[ :recovery ]
75
+ return
76
+ end
77
+
78
+ puts_line "OK: local #{result[ :main_branch ]} is now in sync with #{result[ :remote ]}/#{result[ :main_branch ]}."
79
+ end
80
+
37
81
  # Returns ahead/behind counts for local main versus configured remote main.
38
82
  def main_sync_counts
39
83
  target = "#{config.main_branch}...#{config.git_remote}/#{config.main_branch}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carson
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang