carson 2.11.2 → 2.12.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 +4 -4
- data/API.md +21 -24
- data/MANUAL.md +14 -15
- data/README.md +6 -7
- data/RELEASE.md +38 -0
- data/SKILL.md +2 -2
- data/VERSION +1 -1
- data/carson.gemspec +2 -2
- data/lib/carson/cli.rb +7 -7
- data/lib/carson/config.rb +29 -90
- data/lib/carson/runtime/audit.rb +89 -123
- data/lib/carson/runtime/lint.rb +28 -52
- data/lib/carson/runtime/local.rb +5 -1
- data/lib/carson/runtime/setup.rb +34 -0
- data/templates/.github/workflows/carson-lint.yml +23 -0
- metadata +8 -7
- data/lib/carson/policy/ruby/lint.rb +0 -60
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87aef62f0df0f4a298b22d5fd2dcc033b3fb8ff382e7c43fcc37399fb9e28b17
|
|
4
|
+
data.tar.gz: 50120a7c2252236e86736fbab59eeade47b80488c89b2901849b16dc77ed2c7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 790d1b70d2df69440acc5c7d13ed7566b243e3b69a7f6d5735bc6713c275702e72273cd13ff6c958e3635669ea2411af6289eefc69e6782d3182a7dd390e1f61
|
|
7
|
+
data.tar.gz: e9367dd0605946d9a2f7202c0f41cb2ebebfe6e19b463e606bb88e4e1d719b3c5fbdfc76911bf8b7be1a225f5dbbdd4518b88d5f9f0a5a92ad9a841d11f1f92d
|
data/API.md
CHANGED
|
@@ -16,7 +16,7 @@ carson <command> [subcommand] [arguments]
|
|
|
16
16
|
| Command | Purpose |
|
|
17
17
|
|---|---|
|
|
18
18
|
| `carson setup` | Interactive quiz to configure remote, main branch, workflow, and merge method. Writes `~/.carson/config.json`. |
|
|
19
|
-
| `carson lint
|
|
19
|
+
| `carson lint policy --source <path-or-git-url> [--ref <git-ref>] [--force]` | Distribute lint configs from a central source into the governed repo's `.github/linters/`. |
|
|
20
20
|
| `carson onboard [repo_path]` | Apply one-command baseline setup for a target git repository. Auto-triggers `setup` on first run. |
|
|
21
21
|
| `carson prepare` | Install or refresh Carson-managed global hooks. |
|
|
22
22
|
| `carson refresh [repo_path]` | Re-apply hooks, templates, and audit after upgrading Carson. |
|
|
@@ -78,6 +78,7 @@ Allowed Carson-managed persistence in host repositories:
|
|
|
78
78
|
- `.github/CLAUDE.md` — agent discovery pointer for Claude Code
|
|
79
79
|
- `.github/AGENTS.md` — agent discovery pointer for Codex
|
|
80
80
|
- `.github/pull_request_template.md` — PR template
|
|
81
|
+
- `.github/workflows/carson-lint.yml` — MegaLinter CI workflow
|
|
81
82
|
|
|
82
83
|
## Configuration interface
|
|
83
84
|
|
|
@@ -97,33 +98,31 @@ Environment overrides:
|
|
|
97
98
|
- `CARSON_REVIEW_SWEEP_STATES`
|
|
98
99
|
- `CARSON_WORKFLOW_STYLE`
|
|
99
100
|
- `CARSON_RUBY_INDENTATION`
|
|
101
|
+
- `CARSON_LINT_COMMAND`
|
|
102
|
+
- `CARSON_LINT_ENFORCEMENT`
|
|
103
|
+
- `CARSON_LINT_POLICY_SOURCE`
|
|
100
104
|
|
|
101
|
-
`lint
|
|
105
|
+
`lint` schema:
|
|
102
106
|
|
|
103
107
|
```json
|
|
104
108
|
{
|
|
105
109
|
"lint": {
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"globs": ["**/*.rb"],
|
|
110
|
-
"command": ["ruby", "/absolute/path/to/carson/lib/carson/policy/ruby/lint.rb", "{files}"],
|
|
111
|
-
"config_files": ["~/.carson/lint/rubocop.yml"]
|
|
112
|
-
}
|
|
113
|
-
}
|
|
110
|
+
"command": "make lint",
|
|
111
|
+
"enforcement": "strict",
|
|
112
|
+
"policy_source": "wanghailei/lint.git"
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
```
|
|
117
116
|
|
|
118
|
-
`lint
|
|
119
|
-
- `
|
|
120
|
-
- `
|
|
121
|
-
- `
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
117
|
+
`lint` semantics:
|
|
118
|
+
- `command`: string or array — local lint command executed during `carson audit`. If `null` (default), local lint is skipped.
|
|
119
|
+
- `enforcement`: `"strict"` (default) blocks on lint failure; `"advisory"` warns but does not block.
|
|
120
|
+
- `policy_source`: default source for `carson lint policy` when `--source` is not specified.
|
|
121
|
+
|
|
122
|
+
Environment overrides:
|
|
123
|
+
- `CARSON_LINT_COMMAND` — overrides `lint.command`.
|
|
124
|
+
- `CARSON_LINT_ENFORCEMENT` — overrides `lint.enforcement`.
|
|
125
|
+
- `CARSON_LINT_POLICY_SOURCE` — overrides `lint.policy_source`.
|
|
127
126
|
|
|
128
127
|
Lint target file source precedence in `carson audit`:
|
|
129
128
|
- staged files for local commit-time execution.
|
|
@@ -131,14 +130,12 @@ Lint target file source precedence in `carson audit`:
|
|
|
131
130
|
- full repository tracked files in GitHub non-PR events.
|
|
132
131
|
- local working-tree changes as fallback.
|
|
133
132
|
|
|
134
|
-
Private-source clone token for `carson lint
|
|
133
|
+
Private-source clone token for `carson lint policy`:
|
|
135
134
|
- `CARSON_READ_TOKEN` (used when `--source` points to a private GitHub repository).
|
|
136
135
|
|
|
137
|
-
Ruby source requirement for `carson lint setup` (when Ruby lint is enabled):
|
|
138
|
-
- `CODING/rubocop.yml` must exist in the source tree.
|
|
139
|
-
|
|
140
136
|
Policy layout requirement:
|
|
141
|
-
-
|
|
137
|
+
- Lint config files sit at the root of the source repo and are copied to `<governed-repo>/.github/linters/`.
|
|
138
|
+
- MegaLinter auto-discovers configs in `.github/linters/` during CI.
|
|
142
139
|
|
|
143
140
|
## Output interface
|
|
144
141
|
|
data/MANUAL.md
CHANGED
|
@@ -27,22 +27,20 @@ carson version
|
|
|
27
27
|
|
|
28
28
|
### Step 1: Prepare your lint policy
|
|
29
29
|
|
|
30
|
-
Carson
|
|
31
|
-
|
|
32
|
-
Run `lint setup` to copy policy files into `~/.carson/lint/`:
|
|
30
|
+
Carson distributes lint configuration files from a central policy source — a directory or git repository you control — into each governed repo's `.github/linters/` directory, where MegaLinter auto-discovers them.
|
|
33
31
|
|
|
34
32
|
```bash
|
|
35
|
-
carson lint
|
|
33
|
+
carson lint policy --source /path/to/your-policy-repo
|
|
36
34
|
```
|
|
37
35
|
|
|
38
|
-
After this command,
|
|
36
|
+
After this command, `.github/linters/` contains your lint configs (`.rubocop.yml`, `biome.json`, `ruff.toml`, etc.). MegaLinter uses these in CI. Every governed repository gets the same rules — this is how Carson keeps lint consistent across languages.
|
|
39
37
|
|
|
40
38
|
Options:
|
|
41
39
|
- `--source <path-or-git-url>` — where to read policy files from (required).
|
|
42
40
|
- `--ref <git-ref>` — branch or tag when `--source` is a git URL.
|
|
43
|
-
- `--force` — overwrite existing
|
|
41
|
+
- `--force` — overwrite existing `.github/linters/` files.
|
|
44
42
|
|
|
45
|
-
Policy layout:
|
|
43
|
+
Policy layout: lint config files sit at the root of the source repo (flat layout, no subdirectories required).
|
|
46
44
|
|
|
47
45
|
### Step 2: Onboard a repository
|
|
48
46
|
|
|
@@ -105,7 +103,7 @@ Notes:
|
|
|
105
103
|
|
|
106
104
|
```bash
|
|
107
105
|
carson sync # fast-forward local main
|
|
108
|
-
carson lint
|
|
106
|
+
carson lint policy --source /path/to/your-policy-repo # refresh policy if needed
|
|
109
107
|
carson audit # full governance check
|
|
110
108
|
```
|
|
111
109
|
|
|
@@ -183,7 +181,7 @@ Carson's `govern.merge.method` controls how `carson govern` merges ready PRs. Th
|
|
|
183
181
|
These define what Carson *is*. They are not configurable.
|
|
184
182
|
|
|
185
183
|
- **Outsider boundary** — Carson never places its own artefacts inside a governed repository.
|
|
186
|
-
- **Centralised lint** — one policy source
|
|
184
|
+
- **Centralised lint** — one policy source distributed into each repo's `.github/linters/`, zero per-repo drift.
|
|
187
185
|
- **Active review** — undisposed reviewer findings block merge; feedback must be acknowledged.
|
|
188
186
|
- **Self-diagnosing output** — every message names the cause and the fix.
|
|
189
187
|
- **Transparent governance** — Carson prepares everything for merge but never makes decisions without telling you.
|
|
@@ -238,11 +236,12 @@ Change: `CARSON_HOOKS_BASE_PATH`.
|
|
|
238
236
|
|
|
239
237
|
#### Lint policy source
|
|
240
238
|
|
|
241
|
-
Where lint configuration files
|
|
239
|
+
Where lint configuration files come from and where they land.
|
|
242
240
|
|
|
243
|
-
- Default:
|
|
241
|
+
- Default source: **`wanghailei/lint.git`**. A central repository containing lint configs for all languages.
|
|
242
|
+
- Target: **`<repo>/.github/linters/`**. MegaLinter auto-discovers configs here in CI.
|
|
244
243
|
|
|
245
|
-
Change
|
|
244
|
+
Change: `carson lint policy --source <path-or-git-url>` or `lint.policy_source` in config.
|
|
246
245
|
|
|
247
246
|
#### Scope integrity
|
|
248
247
|
|
|
@@ -314,7 +313,7 @@ Common environment overrides:
|
|
|
314
313
|
| `CARSON_WORKFLOW_STYLE` | Workflow style override (`branch` or `trunk`). |
|
|
315
314
|
| `CARSON_RUBY_INDENTATION` | Ruby indentation policy (`tabs`, `spaces`, or `either`). |
|
|
316
315
|
|
|
317
|
-
For the full configuration schema
|
|
316
|
+
For the full configuration schema, see `API.md`.
|
|
318
317
|
|
|
319
318
|
## Troubleshooting
|
|
320
319
|
|
|
@@ -333,8 +332,8 @@ carson template apply
|
|
|
333
332
|
carson template check
|
|
334
333
|
```
|
|
335
334
|
|
|
336
|
-
**Audit blocks on
|
|
337
|
-
-
|
|
335
|
+
**Audit blocks on lint failure**
|
|
336
|
+
- If `lint.command` is configured and fails, audit blocks (in `strict` mode) or warns (in `advisory` mode). Fix the lint issues and re-run `carson audit`.
|
|
338
337
|
|
|
339
338
|
**Hook version mismatch after upgrade**
|
|
340
339
|
- Run `carson refresh` to re-apply hooks and templates for the new Carson version.
|
data/README.md
CHANGED
|
@@ -22,7 +22,6 @@ Carson is an autonomous governance runtime that lives on your workstation and in
|
|
|
22
22
|
│ │
|
|
23
23
|
│ ~/.carson/ Carson config │
|
|
24
24
|
│ ~/.carson/hooks/ Git hooks │
|
|
25
|
-
│ ~/.carson/lint/ Lint policy │
|
|
26
25
|
│ ~/.carson/cache/ Reports │
|
|
27
26
|
│ ~/.carson/govern/ Dispatch state │
|
|
28
27
|
│ │
|
|
@@ -45,7 +44,7 @@ This separation is Carson's defining trait — the **outsider boundary**: no Car
|
|
|
45
44
|
Carson is opinionated about governance. These are non-negotiable principles, not configurable defaults:
|
|
46
45
|
|
|
47
46
|
- **Outsider boundary** — Carson lives outside your repo, never inside. No Carson-owned artefacts in your repository. Offboarding leaves no trace.
|
|
48
|
-
- **Centralised lint** — lint policy
|
|
47
|
+
- **Centralised lint** — lint policy distributed from a central source into each repo's `.github/linters/`. One source of truth, zero drift.
|
|
49
48
|
- **Active review** — undisposed reviewer findings block merge. Feedback must be acknowledged, not buried.
|
|
50
49
|
- **Self-diagnosing output** — every message names the cause and the fix. If you need to debug Carson's output, the output failed.
|
|
51
50
|
- **Transparent governance** — Carson prepares everything for merge but never oversteps. It does not make decisions for you without telling you.
|
|
@@ -54,8 +53,8 @@ Everything else — workflow style, merge method, remote name, main branch — i
|
|
|
54
53
|
|
|
55
54
|
The data flow:
|
|
56
55
|
|
|
57
|
-
1. You maintain a **policy source** — a directory or git repository containing your lint
|
|
58
|
-
2. `carson onboard` installs git hooks, synchronises `.github/*` templates, and runs a first governance audit on a host repository.
|
|
56
|
+
1. You maintain a **policy source** — a directory or git repository containing your lint config files (e.g. `.rubocop.yml`, `biome.json`, `ruff.toml`). `carson lint policy --source <repo>` copies these into each governed repo's `.github/linters/`, where MegaLinter auto-discovers them.
|
|
57
|
+
2. `carson onboard` installs git hooks, synchronises `.github/*` templates (including a MegaLinter CI workflow), and runs a first governance audit on a host repository.
|
|
59
58
|
3. From that point, every commit triggers `carson audit` through the managed `pre-commit` hook. The same `carson audit` runs in GitHub Actions. If it passes locally, it passes in CI.
|
|
60
59
|
4. `carson review gate` enforces review accountability: it blocks merge until every actionable reviewer comment has been formally acknowledged by the PR author through a **disposition comment**.
|
|
61
60
|
5. `carson govern` triages all open PRs across your portfolio. Ready PRs are merged and housekept. Failing PRs get a coding agent dispatched to fix them. Stuck PRs are escalated for your attention.
|
|
@@ -75,7 +74,7 @@ The data flow:
|
|
|
75
74
|
|
|
76
75
|
| Command | What it does |
|
|
77
76
|
|---|---|
|
|
78
|
-
| `carson lint
|
|
77
|
+
| `carson lint policy --source <repo>` | Distribute lint configs from policy source into `.github/linters/`. |
|
|
79
78
|
| `carson onboard` | One-command baseline: hooks + templates + first audit. |
|
|
80
79
|
| `carson prepare` | Install or refresh Carson-managed global hooks. |
|
|
81
80
|
| `carson refresh` | Re-apply hooks, templates, and audit after upgrading Carson. |
|
|
@@ -116,10 +115,10 @@ gem install --user-install carson
|
|
|
116
115
|
carson version
|
|
117
116
|
```
|
|
118
117
|
|
|
119
|
-
**Prepare your lint policy.** A policy source is any directory (or git URL)
|
|
118
|
+
**Prepare your lint policy.** A policy source is any directory (or git URL) containing your lint configuration files (`.rubocop.yml`, `biome.json`, `ruff.toml`, etc.). Carson copies these into the governed repo's `.github/linters/` where MegaLinter auto-discovers them:
|
|
120
119
|
|
|
121
120
|
```bash
|
|
122
|
-
carson lint
|
|
121
|
+
carson lint policy --source /path/to/your-policy-repo
|
|
123
122
|
```
|
|
124
123
|
|
|
125
124
|
**Onboard a repository:**
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,44 @@ 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
|
+
## 2.12.0 — Language-Agnostic Lint Policy Distribution + MegaLinter
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Lint policy distribution is now language-agnostic.** `carson lint policy --source <path-or-git-url>` copies all files from the source repo root into the governed repo's `.github/linters/` directory, where MegaLinter auto-discovers them. Works for any linter config: rubocop.yml, biome.json, ruff.toml, .erb-lint.yml, etc.
|
|
13
|
+
- **New `lint.command` config key.** Local audit lint is now a single user-configured command (e.g. `"make lint"`, `"trunk check"`, `["ruff", "check"]`). Replaces the old per-language `lint.languages` system entirely.
|
|
14
|
+
- **New `lint.enforcement` config key.** `"strict"` (default) blocks on lint failure; `"advisory"` warns but does not block.
|
|
15
|
+
- **New `lint.policy_source` config key.** Default: `wanghailei/lint.git`. Sets the default source for lint policy distribution.
|
|
16
|
+
- **MegaLinter CI workflow template.** `carson onboard` now installs `.github/workflows/carson-lint.yml`, which runs MegaLinter on PRs and pushes to main.
|
|
17
|
+
- **Interactive setup prompts.** `carson setup` now asks for lint command and enforcement mode.
|
|
18
|
+
- **Removed `lint.languages`** — all per-language lint configuration, Ruby-specific lint runners, and hardcoded language definitions are gone.
|
|
19
|
+
- **Removed `lint setup` subcommand alias** — use `carson lint policy` instead.
|
|
20
|
+
- **Removed `--legacy` flag** from `carson lint policy`.
|
|
21
|
+
- **Source repo layout simplified** — lint config files live at the source repo root; no `CODING/` subdirectory required.
|
|
22
|
+
|
|
23
|
+
### Breaking changes
|
|
24
|
+
|
|
25
|
+
- `lint.languages` config key no longer exists. If your config references it, remove it.
|
|
26
|
+
- `carson lint setup` no longer works. Use `carson lint policy --source <path-or-git-url>`.
|
|
27
|
+
- Lint policy files are now written to `<repo>/.github/linters/` (not `~/.carson/lint/`).
|
|
28
|
+
|
|
29
|
+
### What users must do now
|
|
30
|
+
|
|
31
|
+
1. Upgrade Carson to `2.12.0` and run `carson refresh`.
|
|
32
|
+
2. Remove any `lint.languages` entries from your Carson config.
|
|
33
|
+
3. Set `lint.command` in your config if you want local lint during audit (e.g. `"make lint"`).
|
|
34
|
+
4. Run `carson lint policy --source <your-policy-repo>` to distribute linter configs to `.github/linters/`.
|
|
35
|
+
|
|
36
|
+
## 2.11.3 — Refine RubyGems Description Tone
|
|
37
|
+
|
|
38
|
+
### What changed
|
|
39
|
+
|
|
40
|
+
- **Gemspec** — rewrote summary and description with engineer-professional tone. Factual, concrete, no slogans.
|
|
41
|
+
|
|
42
|
+
### What users must do now
|
|
43
|
+
|
|
44
|
+
Nothing. Metadata only.
|
|
45
|
+
|
|
8
46
|
## 2.11.2 — Improve RubyGems Summary and Description
|
|
9
47
|
|
|
10
48
|
### What changed
|
data/SKILL.md
CHANGED
|
@@ -33,7 +33,7 @@ When you see exit 2, do NOT bypass it. Read the output, fix the root cause, and
|
|
|
33
33
|
Carson audit output is structured as labelled key-value lines prefixed with ⧓. Key sections:
|
|
34
34
|
|
|
35
35
|
- **Working Tree** — staged/unstaged status.
|
|
36
|
-
- **Local Lint Quality** —
|
|
36
|
+
- **Local Lint Quality** — lint command result. `lint_command_status: ok` means clean.
|
|
37
37
|
- **Main Sync Status** — whether local main matches remote. If ahead, reset drift before committing.
|
|
38
38
|
- **Scope Integrity Guard** — checks that commits stay within a single business intent and scope group.
|
|
39
39
|
- **Audit Result** — final verdict: `status: ok` (clean), `status: attention` (advisory, not blocking), `status: block` (must fix).
|
|
@@ -99,4 +99,4 @@ Check that `govern.merge.method` in config matches what GitHub allows. If the re
|
|
|
99
99
|
- Carson never lives inside governed repositories. No `.carson.yml`, no `bin/carson`, no `.tools/carson/`.
|
|
100
100
|
- Carson-managed files in repos are limited to `.github/*` templates.
|
|
101
101
|
- Carson's hooks live at `~/.carson/hooks/<version>/`, never in `.git/hooks/`.
|
|
102
|
-
- Lint policy
|
|
102
|
+
- Lint policy is distributed via `carson lint policy --source <policy-repo>` into each repo's `.github/linters/`.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.12.0
|
data/carson.gemspec
CHANGED
|
@@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.version = Carson::VERSION
|
|
8
8
|
spec.authors = [ "Hailei Wang" ]
|
|
9
9
|
spec.email = [ "wanghailei@users.noreply.github.com" ]
|
|
10
|
-
spec.summary = "
|
|
11
|
-
spec.description = "Carson
|
|
10
|
+
spec.summary = "Autonomous governance runtime — lint, review gates, PR triage, and merge across repositories."
|
|
11
|
+
spec.description = "Carson lives outside the repositories it governs. On every commit it enforces centralised lint policy and scope checks. On PRs it gates merge on unresolved reviewer feedback, dispatches coding agents to fix CI failures, merges passing PRs, and housekeeps branches. Runs locally and in GitHub Actions."
|
|
12
12
|
spec.homepage = "https://github.com/wanghailei/carson"
|
|
13
13
|
spec.license = "MIT"
|
|
14
14
|
spec.required_ruby_version = ">= 3.4"
|
data/lib/carson/cli.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Carson
|
|
|
47
47
|
|
|
48
48
|
def self.build_parser
|
|
49
49
|
OptionParser.new do |opts|
|
|
50
|
-
opts.banner = "Usage: carson [setup|audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [repo_path]|offboard [repo_path]|template check|template apply|lint
|
|
50
|
+
opts.banner = "Usage: carson [setup|audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [repo_path]|offboard [repo_path]|template check|template apply|lint policy --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
@@ -112,8 +112,8 @@ module Carson
|
|
|
112
112
|
|
|
113
113
|
def self.parse_lint_subcommand( argv:, parser:, err: )
|
|
114
114
|
action = argv.shift
|
|
115
|
-
unless action == "
|
|
116
|
-
err.puts "#{BADGE} Missing or invalid subcommand for lint. Use: carson lint
|
|
115
|
+
unless action == "policy"
|
|
116
|
+
err.puts "#{BADGE} Missing or invalid subcommand for lint. Use: carson lint policy --source <path-or-git-url> [--ref <git-ref>] [--force]"
|
|
117
117
|
err.puts parser
|
|
118
118
|
return { command: :invalid }
|
|
119
119
|
end
|
|
@@ -124,19 +124,19 @@ module Carson
|
|
|
124
124
|
force: false
|
|
125
125
|
}
|
|
126
126
|
lint_parser = OptionParser.new do |opts|
|
|
127
|
-
opts.banner = "Usage: carson lint
|
|
127
|
+
opts.banner = "Usage: carson lint policy --source <path-or-git-url> [--ref <git-ref>] [--force]"
|
|
128
128
|
opts.on( "--source SOURCE", "Source repository path or git URL that contains CODING/" ) { |value| options[ :source ] = value.to_s.strip }
|
|
129
129
|
opts.on( "--ref REF", "Git ref used when --source is a git URL (default: main)" ) { |value| options[ :ref ] = value.to_s.strip }
|
|
130
|
-
opts.on( "--force", "Overwrite existing files
|
|
130
|
+
opts.on( "--force", "Overwrite existing files" ) { options[ :force ] = true }
|
|
131
131
|
end
|
|
132
132
|
lint_parser.parse!( argv )
|
|
133
133
|
if options.fetch( :source ).to_s.empty?
|
|
134
|
-
err.puts "#{BADGE} Missing required --source for lint
|
|
134
|
+
err.puts "#{BADGE} Missing required --source for lint policy."
|
|
135
135
|
err.puts lint_parser
|
|
136
136
|
return { command: :invalid }
|
|
137
137
|
end
|
|
138
138
|
unless argv.empty?
|
|
139
|
-
err.puts "#{BADGE} Unexpected arguments for lint
|
|
139
|
+
err.puts "#{BADGE} Unexpected arguments for lint policy: #{argv.join( ' ' )}"
|
|
140
140
|
err.puts lint_parser
|
|
141
141
|
return { command: :invalid }
|
|
142
142
|
end
|
data/lib/carson/config.rb
CHANGED
|
@@ -8,7 +8,8 @@ module Carson
|
|
|
8
8
|
class Config
|
|
9
9
|
attr_accessor :git_remote
|
|
10
10
|
attr_reader :main_branch, :protected_branches, :hooks_base_path, :required_hooks,
|
|
11
|
-
:path_groups, :template_managed_files,
|
|
11
|
+
:path_groups, :template_managed_files,
|
|
12
|
+
:lint_command, :lint_enforcement, :lint_policy_source,
|
|
12
13
|
:review_wait_seconds, :review_poll_seconds, :review_max_polls, :review_sweep_window_days,
|
|
13
14
|
:review_sweep_states, :review_disposition_prefix, :review_risk_keywords,
|
|
14
15
|
:review_tracking_issue_title, :review_tracking_issue_label, :review_bot_usernames,
|
|
@@ -47,10 +48,12 @@ module Carson
|
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
"template" => {
|
|
50
|
-
"managed_files" => [ ".github/carson-instructions.md", ".github/copilot-instructions.md", ".github/CLAUDE.md", ".github/AGENTS.md", ".github/pull_request_template.md" ]
|
|
51
|
+
"managed_files" => [ ".github/carson-instructions.md", ".github/copilot-instructions.md", ".github/CLAUDE.md", ".github/AGENTS.md", ".github/pull_request_template.md", ".github/workflows/carson-lint.yml" ]
|
|
51
52
|
},
|
|
52
53
|
"lint" => {
|
|
53
|
-
"
|
|
54
|
+
"command" => nil,
|
|
55
|
+
"enforcement" => "strict",
|
|
56
|
+
"policy_source" => "wanghailei/lint.git"
|
|
54
57
|
},
|
|
55
58
|
"workflow" => {
|
|
56
59
|
"style" => "branch"
|
|
@@ -94,42 +97,6 @@ module Carson
|
|
|
94
97
|
}
|
|
95
98
|
end
|
|
96
99
|
|
|
97
|
-
def self.default_lint_languages_data
|
|
98
|
-
ruby_runner = File.expand_path( "policy/ruby/lint.rb", __dir__ )
|
|
99
|
-
{
|
|
100
|
-
"ruby" => {
|
|
101
|
-
"enabled" => true,
|
|
102
|
-
"globs" => [ "**/*.rb", "Gemfile", "*.gemspec", "Rakefile" ],
|
|
103
|
-
"command" => [ "ruby", ruby_runner, "{files}" ],
|
|
104
|
-
"config_files" => [ "~/.carson/lint/rubocop.yml" ]
|
|
105
|
-
},
|
|
106
|
-
"javascript" => {
|
|
107
|
-
"enabled" => false,
|
|
108
|
-
"globs" => [ "**/*.js", "**/*.mjs", "**/*.cjs", "**/*.jsx" ],
|
|
109
|
-
"command" => [ "node", "~/.carson/lint/javascript.lint.js", "{files}" ],
|
|
110
|
-
"config_files" => [ "~/.carson/lint/javascript.lint.js" ]
|
|
111
|
-
},
|
|
112
|
-
"css" => {
|
|
113
|
-
"enabled" => false,
|
|
114
|
-
"globs" => [ "**/*.css" ],
|
|
115
|
-
"command" => [ "node", "~/.carson/lint/css.lint.js", "{files}" ],
|
|
116
|
-
"config_files" => [ "~/.carson/lint/css.lint.js" ]
|
|
117
|
-
},
|
|
118
|
-
"html" => {
|
|
119
|
-
"enabled" => false,
|
|
120
|
-
"globs" => [ "**/*.html" ],
|
|
121
|
-
"command" => [ "node", "~/.carson/lint/html.lint.js", "{files}" ],
|
|
122
|
-
"config_files" => [ "~/.carson/lint/html.lint.js" ]
|
|
123
|
-
},
|
|
124
|
-
"erb" => {
|
|
125
|
-
"enabled" => false,
|
|
126
|
-
"globs" => [ "**/*.erb" ],
|
|
127
|
-
"command" => [ "ruby", "~/.carson/lint/erb.lint.rb", "{files}" ],
|
|
128
|
-
"config_files" => [ "~/.carson/lint/erb.lint.rb" ]
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
end
|
|
132
|
-
|
|
133
100
|
def self.load_global_config_data( repo_root: )
|
|
134
101
|
path = global_config_path( repo_root: repo_root )
|
|
135
102
|
return {} if path.empty? || !File.file?( path )
|
|
@@ -200,6 +167,13 @@ module Carson
|
|
|
200
167
|
audit = fetch_hash_section( data: copy, key: "audit" )
|
|
201
168
|
advisory_names = env_string_array( key: "CARSON_AUDIT_ADVISORY_CHECK_NAMES" )
|
|
202
169
|
audit[ "advisory_check_names" ] = advisory_names unless advisory_names.empty?
|
|
170
|
+
lint = fetch_hash_section( data: copy, key: "lint" )
|
|
171
|
+
lint_command_env = ENV.fetch( "CARSON_LINT_COMMAND", "" ).to_s.strip
|
|
172
|
+
lint[ "command" ] = lint_command_env unless lint_command_env.empty?
|
|
173
|
+
lint_enforcement_env = ENV.fetch( "CARSON_LINT_ENFORCEMENT", "" ).to_s.strip
|
|
174
|
+
lint[ "enforcement" ] = lint_enforcement_env unless lint_enforcement_env.empty?
|
|
175
|
+
lint_policy_source_env = ENV.fetch( "CARSON_LINT_POLICY_SOURCE", "" ).to_s.strip
|
|
176
|
+
lint[ "policy_source" ] = lint_policy_source_env unless lint_policy_source_env.empty?
|
|
203
177
|
style = fetch_hash_section( data: copy, key: "style" )
|
|
204
178
|
ruby_indentation = ENV.fetch( "CARSON_RUBY_INDENTATION", "" ).to_s.strip
|
|
205
179
|
style[ "ruby_indentation" ] = ruby_indentation unless ruby_indentation.empty?
|
|
@@ -248,9 +222,10 @@ module Carson
|
|
|
248
222
|
@path_groups = fetch_hash( hash: fetch_hash( hash: data, key: "scope" ), key: "path_groups" ).transform_values { |value| normalize_patterns( value: value ) }
|
|
249
223
|
|
|
250
224
|
@template_managed_files = fetch_string_array( hash: fetch_hash( hash: data, key: "template" ), key: "managed_files" )
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
)
|
|
225
|
+
lint_hash = fetch_hash( hash: data, key: "lint" )
|
|
226
|
+
@lint_command = normalize_lint_command_setting( value: lint_hash[ "command" ] )
|
|
227
|
+
@lint_enforcement = normalize_lint_enforcement( value: lint_hash.fetch( "enforcement", "strict" ) )
|
|
228
|
+
@lint_policy_source = lint_hash.fetch( "policy_source", "" ).to_s.strip
|
|
254
229
|
|
|
255
230
|
workflow_hash = fetch_hash( hash: data, key: "workflow" )
|
|
256
231
|
@workflow_style = fetch_string( hash: workflow_hash, key: "style" ).downcase
|
|
@@ -296,7 +271,6 @@ module Carson
|
|
|
296
271
|
raise ConfigError, "hooks.base_path cannot be empty" if hooks_base_path.empty?
|
|
297
272
|
raise ConfigError, "hooks.required_hooks cannot be empty" if required_hooks.empty?
|
|
298
273
|
raise ConfigError, "scope.path_groups cannot be empty" if path_groups.empty?
|
|
299
|
-
raise ConfigError, "lint.languages cannot be empty" if lint_languages.empty?
|
|
300
274
|
raise ConfigError, "review.required_disposition_prefix cannot be empty" if review_disposition_prefix.empty?
|
|
301
275
|
raise ConfigError, "review.risk_keywords cannot be empty" if review_risk_keywords.empty?
|
|
302
276
|
raise ConfigError, "review.sweep.states must contain one or both of open, closed" if ( review_sweep_states - [ "open", "closed" ] ).any? || review_sweep_states.empty?
|
|
@@ -364,56 +338,21 @@ module Carson
|
|
|
364
338
|
patterns
|
|
365
339
|
end
|
|
366
340
|
|
|
367
|
-
def
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
raise ConfigError, "lint.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
normalised[ language ] = normalize_lint_language_entry( language: language, raw_entry: raw_entry )
|
|
341
|
+
def normalize_lint_command_setting( value: )
|
|
342
|
+
return nil if value.nil?
|
|
343
|
+
return value.to_s.strip if value.is_a?( String )
|
|
344
|
+
if value.is_a?( Array )
|
|
345
|
+
parts = value.map { |entry| entry.to_s.strip }.reject( &:empty? )
|
|
346
|
+
raise ConfigError, "lint.command array must contain at least one argument" if parts.empty?
|
|
347
|
+
return parts
|
|
376
348
|
end
|
|
377
|
-
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
def normalize_lint_language_entry( language:, raw_entry: )
|
|
381
|
-
{
|
|
382
|
-
enabled: fetch_optional_boolean(
|
|
383
|
-
hash: raw_entry,
|
|
384
|
-
key: "enabled",
|
|
385
|
-
default: true,
|
|
386
|
-
key_path: "lint.languages.#{language}.enabled"
|
|
387
|
-
),
|
|
388
|
-
globs: normalize_lint_globs( language: language, value: raw_entry[ "globs" ] ),
|
|
389
|
-
command: normalize_lint_command( language: language, value: raw_entry[ "command" ] ),
|
|
390
|
-
config_files: normalize_lint_config_files( language: language, value: raw_entry[ "config_files" ] )
|
|
391
|
-
}
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
def normalize_lint_globs( language:, value: )
|
|
395
|
-
raise ConfigError, "lint.languages.#{language}.globs must be an array" unless value.is_a?( Array )
|
|
396
|
-
patterns = Array( value ).map { |entry| entry.to_s.strip }.reject( &:empty? )
|
|
397
|
-
raise ConfigError, "lint.languages.#{language}.globs must contain at least one pattern" if patterns.empty?
|
|
398
|
-
patterns
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def normalize_lint_command( language:, value: )
|
|
402
|
-
raise ConfigError, "lint.languages.#{language}.command must be an array" unless value.is_a?( Array )
|
|
403
|
-
command = Array( value ).map { |entry| entry.to_s.strip }.reject( &:empty? )
|
|
404
|
-
raise ConfigError, "lint.languages.#{language}.command must contain at least one argument" if command.empty?
|
|
405
|
-
command
|
|
349
|
+
raise ConfigError, "lint.command must be a string, array, or null"
|
|
406
350
|
end
|
|
407
351
|
|
|
408
|
-
def
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
files.map do |path|
|
|
413
|
-
expanded = path.start_with?( "~" ) ? File.expand_path( path ) : path
|
|
414
|
-
raise ConfigError, "lint.languages.#{language}.config_files entries must be absolute paths or ~/ paths" unless expanded.start_with?( "/" )
|
|
415
|
-
expanded
|
|
416
|
-
end
|
|
352
|
+
def normalize_lint_enforcement( value: )
|
|
353
|
+
text = value.to_s.strip.downcase
|
|
354
|
+
raise ConfigError, "lint.enforcement must be one of strict, advisory" unless [ "strict", "advisory" ].include?( text )
|
|
355
|
+
text
|
|
417
356
|
end
|
|
418
357
|
|
|
419
358
|
def fetch_optional_boolean( hash:, key:, default:, key_path: nil )
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -162,9 +162,43 @@ module Carson
|
|
|
162
162
|
report
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
-
# Enforces configured
|
|
165
|
+
# Enforces configured lint policy before governance passes.
|
|
166
|
+
# Runs lint.command and gates on exit code. Skips when lint.command is not set.
|
|
166
167
|
def local_lint_quality_report
|
|
168
|
+
unless config.lint_command
|
|
169
|
+
report = {
|
|
170
|
+
status: "ok",
|
|
171
|
+
skip_reason: "lint.command not configured",
|
|
172
|
+
target_source: "none",
|
|
173
|
+
target_files_count: 0,
|
|
174
|
+
blocking_languages: 0,
|
|
175
|
+
languages: []
|
|
176
|
+
}
|
|
177
|
+
puts_verbose "lint: SKIP (lint.command not configured)"
|
|
178
|
+
return report
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
lint_command_report
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
report = {
|
|
184
|
+
status: "block",
|
|
185
|
+
skip_reason: e.message,
|
|
186
|
+
target_source: "unknown",
|
|
187
|
+
target_files_count: 0,
|
|
188
|
+
blocking_languages: 0,
|
|
189
|
+
languages: []
|
|
190
|
+
}
|
|
191
|
+
puts_line "BLOCK: local lint quality check failed (#{e.message})."
|
|
192
|
+
report
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Runs the lint.command and returns a structured report.
|
|
196
|
+
def lint_command_report
|
|
167
197
|
target_files, target_source = lint_target_files
|
|
198
|
+
advisory = config.lint_enforcement == "advisory"
|
|
199
|
+
command_value = config.lint_command
|
|
200
|
+
command_string = command_value.is_a?( Array ) ? command_value.join( " " ) : command_value.to_s
|
|
201
|
+
|
|
168
202
|
report = {
|
|
169
203
|
status: "ok",
|
|
170
204
|
skip_reason: nil,
|
|
@@ -175,32 +209,63 @@ module Carson
|
|
|
175
209
|
}
|
|
176
210
|
puts_verbose "lint_target_source: #{target_source}"
|
|
177
211
|
puts_verbose "lint_target_files_total: #{target_files.count}"
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
language: language,
|
|
181
|
-
entry: entry,
|
|
182
|
-
target_files: target_files
|
|
183
|
-
)
|
|
184
|
-
report.fetch( :languages ) << language_report
|
|
185
|
-
next unless language_report.fetch( :status ) == "block"
|
|
212
|
+
puts_verbose "lint_command: #{command_string}"
|
|
213
|
+
puts_verbose "lint_enforcement: #{config.lint_enforcement}"
|
|
186
214
|
|
|
187
|
-
|
|
188
|
-
|
|
215
|
+
args = command_string.split( /\s+/ )
|
|
216
|
+
command_name = args.first.to_s.strip
|
|
217
|
+
unless command_available_for_lint?( command_name: command_name )
|
|
218
|
+
language_report = {
|
|
219
|
+
language: "lint.command",
|
|
220
|
+
enabled: true,
|
|
221
|
+
status: "block",
|
|
222
|
+
reason: "command not available: #{command_name}",
|
|
223
|
+
file_count: target_files.count,
|
|
224
|
+
files: target_files,
|
|
225
|
+
command: args,
|
|
226
|
+
config_files: [],
|
|
227
|
+
exit_code: EXIT_BLOCK
|
|
228
|
+
}
|
|
229
|
+
report[ :languages ] << language_report
|
|
230
|
+
report[ :status ] = advisory ? "ok" : "block"
|
|
231
|
+
report[ :blocking_languages ] = advisory ? 0 : 1
|
|
232
|
+
puts_verbose "lint_command_status: #{language_report.fetch( :status )}"
|
|
233
|
+
puts_line "WARN: lint command not available: #{command_name}" if advisory
|
|
234
|
+
return report
|
|
189
235
|
end
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
236
|
+
|
|
237
|
+
stdout_text, stderr_text, success, exit_code = local_command( *args )
|
|
238
|
+
language_report = {
|
|
239
|
+
language: "lint.command",
|
|
240
|
+
enabled: true,
|
|
241
|
+
status: success ? "ok" : "block",
|
|
242
|
+
reason: success ? nil : summarise_command_output(
|
|
243
|
+
stdout_text: stdout_text,
|
|
244
|
+
stderr_text: stderr_text,
|
|
245
|
+
fallback: "lint command failed"
|
|
246
|
+
),
|
|
247
|
+
file_count: target_files.count,
|
|
248
|
+
files: target_files,
|
|
249
|
+
command: args,
|
|
250
|
+
config_files: [],
|
|
251
|
+
exit_code: exit_code
|
|
200
252
|
}
|
|
201
|
-
report[ :
|
|
202
|
-
|
|
203
|
-
|
|
253
|
+
report[ :languages ] << language_report
|
|
254
|
+
|
|
255
|
+
unless success
|
|
256
|
+
if advisory
|
|
257
|
+
report[ :status ] = "ok"
|
|
258
|
+
puts_verbose "lint_command_status: advisory_warn (exit #{exit_code})"
|
|
259
|
+
puts_line "WARN: lint command failed (exit #{exit_code}) — advisory mode, not blocking."
|
|
260
|
+
else
|
|
261
|
+
report[ :status ] = "block"
|
|
262
|
+
report[ :blocking_languages ] = 1
|
|
263
|
+
puts_verbose "lint_command_status: block (exit #{exit_code})"
|
|
264
|
+
end
|
|
265
|
+
else
|
|
266
|
+
puts_verbose "lint_command_status: ok"
|
|
267
|
+
end
|
|
268
|
+
|
|
204
269
|
report
|
|
205
270
|
end
|
|
206
271
|
|
|
@@ -274,85 +339,6 @@ module Carson
|
|
|
274
339
|
end.compact.uniq
|
|
275
340
|
end
|
|
276
341
|
|
|
277
|
-
def lint_language_report( language:, entry:, target_files: )
|
|
278
|
-
globs = entry.fetch( :globs )
|
|
279
|
-
candidate_files = Array( target_files ).select do |path|
|
|
280
|
-
globs.any? { |pattern| pattern_matches_path?( pattern: pattern, path: path ) }
|
|
281
|
-
end
|
|
282
|
-
report = {
|
|
283
|
-
language: language,
|
|
284
|
-
enabled: entry.fetch( :enabled ),
|
|
285
|
-
status: "ok",
|
|
286
|
-
reason: nil,
|
|
287
|
-
file_count: candidate_files.count,
|
|
288
|
-
files: candidate_files,
|
|
289
|
-
command: entry.fetch( :command ),
|
|
290
|
-
config_files: entry.fetch( :config_files ),
|
|
291
|
-
exit_code: 0
|
|
292
|
-
}
|
|
293
|
-
puts_verbose "lint_language: #{language} enabled=#{report.fetch( :enabled )} files=#{report.fetch( :file_count )}"
|
|
294
|
-
if language == "ruby" && outsider_mode?
|
|
295
|
-
local_rubocop_path = File.join( repo_root, ".rubocop.yml" )
|
|
296
|
-
if File.file?( local_rubocop_path )
|
|
297
|
-
report[ :status ] = "block"
|
|
298
|
-
report[ :reason ] = "repo-local RuboCop config is forbidden: #{relative_path( local_rubocop_path )}; remove it and use ~/.carson/lint/rubocop.yml."
|
|
299
|
-
report[ :exit_code ] = EXIT_BLOCK
|
|
300
|
-
puts_verbose "lint_#{language}_status: block"
|
|
301
|
-
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
302
|
-
puts_verbose "ACTION: remove .rubocop.yml from this repository and run carson lint setup --source <path-or-git-url>."
|
|
303
|
-
return report
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
return report unless report.fetch( :enabled )
|
|
307
|
-
return report if candidate_files.empty?
|
|
308
|
-
|
|
309
|
-
missing_config_files = entry.fetch( :config_files ).reject { |path| File.file?( path ) }
|
|
310
|
-
unless missing_config_files.empty?
|
|
311
|
-
report[ :status ] = "block"
|
|
312
|
-
report[ :reason ] = "missing config files: #{missing_config_files.join( ', ' )}"
|
|
313
|
-
report[ :exit_code ] = EXIT_BLOCK
|
|
314
|
-
puts_verbose "lint_#{language}_status: block"
|
|
315
|
-
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
316
|
-
puts_verbose "ACTION: run carson lint setup --source <path-or-git-url> to prepare ~/.carson/lint policy files."
|
|
317
|
-
return report
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
command = Array( entry.fetch( :command ) )
|
|
321
|
-
command_name = command.first.to_s.strip
|
|
322
|
-
if command_name.empty?
|
|
323
|
-
report[ :status ] = "block"
|
|
324
|
-
report[ :reason ] = "missing lint command"
|
|
325
|
-
report[ :exit_code ] = EXIT_BLOCK
|
|
326
|
-
puts_verbose "lint_#{language}_status: block"
|
|
327
|
-
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
328
|
-
return report
|
|
329
|
-
end
|
|
330
|
-
unless command_available_for_lint?( command_name: command_name )
|
|
331
|
-
report[ :status ] = "block"
|
|
332
|
-
report[ :reason ] = "command not available: #{command_name}"
|
|
333
|
-
report[ :exit_code ] = EXIT_BLOCK
|
|
334
|
-
puts_verbose "lint_#{language}_status: block"
|
|
335
|
-
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
336
|
-
return report
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
args = expanded_lint_command_args( command: command, files: candidate_files )
|
|
340
|
-
stdout_text, stderr_text, success, exit_code = local_command( *args )
|
|
341
|
-
report[ :exit_code ] = exit_code
|
|
342
|
-
unless success
|
|
343
|
-
report[ :status ] = "block"
|
|
344
|
-
report[ :reason ] = summarise_command_output(
|
|
345
|
-
stdout_text: stdout_text,
|
|
346
|
-
stderr_text: stderr_text,
|
|
347
|
-
fallback: "lint command failed for #{language}"
|
|
348
|
-
)
|
|
349
|
-
end
|
|
350
|
-
puts_verbose "lint_#{language}_status: #{report.fetch( :status )}"
|
|
351
|
-
puts_verbose "lint_#{language}_exit: #{report.fetch( :exit_code )}"
|
|
352
|
-
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}" unless report.fetch( :reason ).nil?
|
|
353
|
-
report
|
|
354
|
-
end
|
|
355
|
-
|
|
356
342
|
def command_available_for_lint?( command_name: )
|
|
357
343
|
return false if command_name.to_s.strip.empty?
|
|
358
344
|
|
|
@@ -373,26 +359,6 @@ module Carson
|
|
|
373
359
|
end
|
|
374
360
|
end
|
|
375
361
|
|
|
376
|
-
def expanded_lint_command_args( command:, files: )
|
|
377
|
-
expanded_command = Array( command ).map do |arg|
|
|
378
|
-
text = arg.to_s
|
|
379
|
-
if text == "{files}"
|
|
380
|
-
text
|
|
381
|
-
elsif text.start_with?( "~" )
|
|
382
|
-
File.expand_path( text )
|
|
383
|
-
elsif text.include?( "/" ) && !text.start_with?( "/" )
|
|
384
|
-
File.expand_path( text, repo_root )
|
|
385
|
-
else
|
|
386
|
-
text
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
if expanded_command.include?( "{files}" )
|
|
390
|
-
return expanded_command.flat_map { |arg| arg == "{files}" ? Array( files ) : arg }
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
expanded_command + Array( files )
|
|
394
|
-
end
|
|
395
|
-
|
|
396
362
|
# Local command runner for repository-context tools used by audit lint checks.
|
|
397
363
|
def local_command( *args )
|
|
398
364
|
stdout_text, stderr_text, status = Open3.capture3( *args, chdir: repo_root )
|
data/lib/carson/runtime/lint.rb
CHANGED
|
@@ -5,13 +5,14 @@ require "tmpdir"
|
|
|
5
5
|
module Carson
|
|
6
6
|
class Runtime
|
|
7
7
|
module Lint
|
|
8
|
-
#
|
|
9
|
-
|
|
8
|
+
# Distributes lint policy files from a central source into the governed repository.
|
|
9
|
+
# Target: <repo>/.github/linters/ (MegaLinter auto-discovers here).
|
|
10
|
+
def lint_setup!( source:, ref: "main", force: false, **_ )
|
|
10
11
|
puts_verbose ""
|
|
11
|
-
puts_verbose "[Lint
|
|
12
|
+
puts_verbose "[Lint Policy]"
|
|
12
13
|
source_text = source.to_s.strip
|
|
13
14
|
if source_text.empty?
|
|
14
|
-
puts_line "ERROR: lint
|
|
15
|
+
puts_line "ERROR: lint policy requires --source <path-or-git-url>."
|
|
15
16
|
return EXIT_ERROR
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -19,40 +20,26 @@ module Carson
|
|
|
19
20
|
ref_text = "main" if ref_text.empty?
|
|
20
21
|
source_dir, cleanup = lint_setup_source_directory( source: source_text, ref: ref_text )
|
|
21
22
|
begin
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
target_coding_dir = ai_coding_dir
|
|
28
|
-
copy_result = copy_lint_coding_tree(
|
|
29
|
-
source_coding_dir: source_coding_dir,
|
|
30
|
-
target_coding_dir: target_coding_dir,
|
|
23
|
+
target_dir = repo_linters_dir
|
|
24
|
+
copy_result = copy_lint_policy_files(
|
|
25
|
+
source_dir: source_dir,
|
|
26
|
+
target_dir: target_dir,
|
|
31
27
|
force: force
|
|
32
28
|
)
|
|
33
|
-
puts_verbose "
|
|
34
|
-
puts_verbose "
|
|
35
|
-
puts_verbose "
|
|
36
|
-
puts_verbose "
|
|
37
|
-
puts_verbose "
|
|
38
|
-
puts_verbose "
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
puts_line "OK: lint policy setup is complete."
|
|
43
|
-
return EXIT_OK
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
missing_policy.each do |entry|
|
|
47
|
-
puts_verbose "missing_lint_policy_file: language=#{entry.fetch( :language )} path=#{entry.fetch( :path )}"
|
|
48
|
-
end
|
|
49
|
-
puts_line "ACTION: update source CODING policy files, rerun carson lint setup, then rerun carson audit."
|
|
50
|
-
EXIT_ERROR
|
|
29
|
+
puts_verbose "lint_policy_source: #{source_text}"
|
|
30
|
+
puts_verbose "lint_policy_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
|
|
31
|
+
puts_verbose "lint_policy_target: #{target_dir}"
|
|
32
|
+
puts_verbose "lint_policy_created: #{copy_result.fetch( :created )}"
|
|
33
|
+
puts_verbose "lint_policy_updated: #{copy_result.fetch( :updated )}"
|
|
34
|
+
puts_verbose "lint_policy_skipped: #{copy_result.fetch( :skipped )}"
|
|
35
|
+
|
|
36
|
+
puts_line "OK: lint policy synced to .github/linters/ (#{copy_result.fetch( :created )} created, #{copy_result.fetch( :updated )} updated)."
|
|
37
|
+
EXIT_OK
|
|
51
38
|
ensure
|
|
52
39
|
cleanup&.call
|
|
53
40
|
end
|
|
54
41
|
rescue StandardError => e
|
|
55
|
-
puts_line "ERROR: lint
|
|
42
|
+
puts_line "ERROR: lint policy failed (#{e.message})"
|
|
56
43
|
EXIT_ERROR
|
|
57
44
|
end
|
|
58
45
|
|
|
@@ -119,22 +106,21 @@ module Carson
|
|
|
119
106
|
"/tmp/carson"
|
|
120
107
|
end
|
|
121
108
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
File.join( home, ".carson", "lint" )
|
|
109
|
+
# Lint configs live inside the governed repository for MegaLinter.
|
|
110
|
+
def repo_linters_dir
|
|
111
|
+
File.join( repo_root, ".github", "linters" )
|
|
127
112
|
end
|
|
128
113
|
|
|
129
|
-
def
|
|
130
|
-
FileUtils.mkdir_p(
|
|
114
|
+
def copy_lint_policy_files( source_dir:, target_dir:, force: )
|
|
115
|
+
FileUtils.mkdir_p( target_dir )
|
|
131
116
|
created = 0
|
|
132
117
|
updated = 0
|
|
133
118
|
skipped = 0
|
|
134
|
-
Dir.glob( "**/*", File::FNM_DOTMATCH, base:
|
|
119
|
+
Dir.glob( "**/*", File::FNM_DOTMATCH, base: source_dir ).sort.each do |relative|
|
|
135
120
|
next if [ ".", ".." ].include?( relative )
|
|
136
|
-
|
|
137
|
-
|
|
121
|
+
next if relative.start_with?( ".git/" ) || relative == ".git"
|
|
122
|
+
source_path = File.join( source_dir, relative )
|
|
123
|
+
target_path = File.join( target_dir, relative )
|
|
138
124
|
if File.directory?( source_path )
|
|
139
125
|
FileUtils.mkdir_p( target_path )
|
|
140
126
|
next
|
|
@@ -161,16 +147,6 @@ module Carson
|
|
|
161
147
|
skipped: skipped
|
|
162
148
|
}
|
|
163
149
|
end
|
|
164
|
-
|
|
165
|
-
def missing_lint_policy_files
|
|
166
|
-
config.lint_languages.each_with_object( [] ) do |( language, entry ), missing|
|
|
167
|
-
next unless entry.fetch( :enabled )
|
|
168
|
-
|
|
169
|
-
entry.fetch( :config_files ).each do |path|
|
|
170
|
-
missing << { language: language, path: path } unless File.file?( path )
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
150
|
end
|
|
175
151
|
|
|
176
152
|
include Lint
|
data/lib/carson/runtime/local.rb
CHANGED
|
@@ -371,7 +371,11 @@ module Carson
|
|
|
371
371
|
|
|
372
372
|
# Calculates whole-file expected content and returns sync status plus apply payload.
|
|
373
373
|
def template_result_for_file( managed_file: )
|
|
374
|
-
|
|
374
|
+
# Try subdirectory-aware path first (e.g. .github/workflows/carson-lint.yml),
|
|
375
|
+
# then fall back to flat basename lookup for backward compatibility.
|
|
376
|
+
relative_within_github = managed_file.delete_prefix( ".github/" )
|
|
377
|
+
template_path = File.join( github_templates_dir, relative_within_github )
|
|
378
|
+
template_path = File.join( github_templates_dir, File.basename( managed_file ) ) unless File.file?( template_path )
|
|
375
379
|
return { file: managed_file, status: "error", reason: "missing template #{File.basename( managed_file )}", applied_content: nil } unless File.file?( template_path )
|
|
376
380
|
|
|
377
381
|
expected_content = normalize_text( text: File.read( template_path ) )
|
data/lib/carson/runtime/setup.rb
CHANGED
|
@@ -39,6 +39,12 @@ module Carson
|
|
|
39
39
|
merge_choice = prompt_merge_method
|
|
40
40
|
choices[ "govern.merge.method" ] = merge_choice unless merge_choice.nil?
|
|
41
41
|
|
|
42
|
+
lint_command_choice = prompt_lint_command
|
|
43
|
+
choices[ "lint.command" ] = lint_command_choice unless lint_command_choice.nil?
|
|
44
|
+
|
|
45
|
+
lint_enforcement_choice = prompt_lint_enforcement
|
|
46
|
+
choices[ "lint.enforcement" ] = lint_enforcement_choice unless lint_enforcement_choice.nil?
|
|
47
|
+
|
|
42
48
|
write_setup_config( choices: choices )
|
|
43
49
|
end
|
|
44
50
|
|
|
@@ -142,6 +148,34 @@ module Carson
|
|
|
142
148
|
prompt_choice( options: options, default: 0 )
|
|
143
149
|
end
|
|
144
150
|
|
|
151
|
+
def prompt_lint_command
|
|
152
|
+
puts_line ""
|
|
153
|
+
puts_line "Lint command"
|
|
154
|
+
options = [
|
|
155
|
+
{ label: "make lint", value: "make lint" },
|
|
156
|
+
{ label: "trunk check (Recommended)", value: "trunk check" },
|
|
157
|
+
{ label: "Other (enter command)", value: :other },
|
|
158
|
+
{ label: "Skip (no local lint)", value: nil }
|
|
159
|
+
]
|
|
160
|
+
choice = prompt_choice( options: options, default: 1 )
|
|
161
|
+
|
|
162
|
+
if choice == :other
|
|
163
|
+
prompt_custom_value( label: "Lint command" )
|
|
164
|
+
else
|
|
165
|
+
choice
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def prompt_lint_enforcement
|
|
170
|
+
puts_line ""
|
|
171
|
+
puts_line "Lint enforcement"
|
|
172
|
+
options = [
|
|
173
|
+
{ label: "strict — block on lint failure (default)", value: "strict" },
|
|
174
|
+
{ label: "advisory — warn but don't block", value: "advisory" }
|
|
175
|
+
]
|
|
176
|
+
prompt_choice( options: options, default: 0 )
|
|
177
|
+
end
|
|
178
|
+
|
|
145
179
|
def prompt_choice( options:, default: )
|
|
146
180
|
options.each_with_index do |option, index|
|
|
147
181
|
puts_line " #{index + 1}) #{option.fetch( :label )}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Carson Lint
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
branches: [main, master]
|
|
5
|
+
push:
|
|
6
|
+
branches: [main, master]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
issues: write
|
|
14
|
+
pull-requests: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
- uses: oxsecurity/megalinter@v8
|
|
20
|
+
env:
|
|
21
|
+
VALIDATE_ALL_CODEBASE: false
|
|
22
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
23
|
+
LINTER_RULES_PATH: .github/linters
|
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: 2.
|
|
4
|
+
version: 2.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -9,10 +9,10 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: Carson
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
description: Carson lives outside the repositories it governs. On every commit it
|
|
13
|
+
enforces centralised lint policy and scope checks. On PRs it gates merge on unresolved
|
|
14
|
+
reviewer feedback, dispatches coding agents to fix CI failures, merges passing PRs,
|
|
15
|
+
and housekeeps branches. Runs locally and in GitHub Actions.
|
|
16
16
|
email:
|
|
17
17
|
- wanghailei@users.noreply.github.com
|
|
18
18
|
executables:
|
|
@@ -46,7 +46,6 @@ files:
|
|
|
46
46
|
- lib/carson/adapters/prompt.rb
|
|
47
47
|
- lib/carson/cli.rb
|
|
48
48
|
- lib/carson/config.rb
|
|
49
|
-
- lib/carson/policy/ruby/lint.rb
|
|
50
49
|
- lib/carson/runtime.rb
|
|
51
50
|
- lib/carson/runtime/audit.rb
|
|
52
51
|
- lib/carson/runtime/govern.rb
|
|
@@ -65,6 +64,7 @@ files:
|
|
|
65
64
|
- templates/.github/carson-instructions.md
|
|
66
65
|
- templates/.github/copilot-instructions.md
|
|
67
66
|
- templates/.github/pull_request_template.md
|
|
67
|
+
- templates/.github/workflows/carson-lint.yml
|
|
68
68
|
homepage: https://github.com/wanghailei/carson
|
|
69
69
|
licenses:
|
|
70
70
|
- MIT
|
|
@@ -93,5 +93,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
93
93
|
requirements: []
|
|
94
94
|
rubygems_version: 4.0.3
|
|
95
95
|
specification_version: 4
|
|
96
|
-
summary:
|
|
96
|
+
summary: Autonomous governance runtime — lint, review gates, PR triage, and merge
|
|
97
|
+
across repositories.
|
|
97
98
|
test_files: []
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
require "open3"
|
|
4
|
-
|
|
5
|
-
EXIT_OK = 0
|
|
6
|
-
EXIT_ERROR = 1
|
|
7
|
-
EXIT_BLOCK = 2
|
|
8
|
-
|
|
9
|
-
def rubocop_config_path
|
|
10
|
-
File.expand_path( "~/.carson/lint/rubocop.yml" )
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def print_stream( io, text )
|
|
14
|
-
content = text.to_s
|
|
15
|
-
return if content.empty?
|
|
16
|
-
io.print( content )
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def run_rubocop( files: )
|
|
20
|
-
stdout_text, stderr_text, status = Open3.capture3(
|
|
21
|
-
"rubocop", "--config", rubocop_config_path, *files
|
|
22
|
-
)
|
|
23
|
-
print_stream( $stdout, stdout_text )
|
|
24
|
-
print_stream( $stderr, stderr_text )
|
|
25
|
-
{ status: :completed, exit_code: status.exitstatus.to_i }
|
|
26
|
-
rescue Errno::ENOENT
|
|
27
|
-
$stderr.puts "ERROR: RuboCop executable `rubocop` is unavailable in PATH. Install the pinned RuboCop gem before running carson audit."
|
|
28
|
-
{ status: :unavailable, exit_code: nil }
|
|
29
|
-
rescue StandardError => e
|
|
30
|
-
$stderr.puts "ERROR: RuboCop execution failed (#{e.message})"
|
|
31
|
-
{ status: :runtime_error, exit_code: nil }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def lint_exit_code( result: )
|
|
35
|
-
case result.fetch( :status )
|
|
36
|
-
when :unavailable
|
|
37
|
-
EXIT_BLOCK
|
|
38
|
-
when :runtime_error
|
|
39
|
-
EXIT_ERROR
|
|
40
|
-
else
|
|
41
|
-
case result.fetch( :exit_code )
|
|
42
|
-
when 0
|
|
43
|
-
EXIT_OK
|
|
44
|
-
when 1
|
|
45
|
-
EXIT_BLOCK
|
|
46
|
-
else
|
|
47
|
-
EXIT_ERROR
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
files = ARGV.map( &:to_s ).map( &:strip ).reject( &:empty? )
|
|
53
|
-
config_path = rubocop_config_path
|
|
54
|
-
unless File.file?( config_path )
|
|
55
|
-
$stderr.puts "ERROR: RuboCop config not found at #{config_path}. Run `carson lint setup --source <path-or-git-url>`."
|
|
56
|
-
exit EXIT_ERROR
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
result = run_rubocop( files: files )
|
|
60
|
-
exit lint_exit_code( result: result )
|