@erclx/aitk 3.52.0 → 3.52.1
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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents/audits.md +1 -1
- package/docs/agents/commands.md +2 -0
- package/docs/agents/gate.md +84 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/markdown-audit.md +3 -3
- package/docs/agents/state-scoped-risk.md +1 -1
- package/package.json +3 -3
- package/scripts/core/list-seed-roots.sh +18 -0
- package/scripts/core/repair-bare-flag.sh +19 -0
- package/scripts/core/update.sh +3 -3
- package/src/audits/catalog.ts +2 -2
- package/src/audits/run.ts +2 -1
- package/src/cli.ts +4 -0
- package/src/commands/claude.ts +2 -2
- package/src/commands/context.ts +2 -2
- package/src/commands/gate.ts +189 -0
- package/src/gate/measures.ts +682 -0
- package/src/gate/sequencer.ts +386 -0
- package/src/gate/stages.ts +412 -0
- package/scripts/core/verify.sh +0 -694
package/docs/agents/audits.md
CHANGED
|
@@ -34,7 +34,7 @@ Each is invoked as the CLI the caller is running rather than as a global `aitk`.
|
|
|
34
34
|
|
|
35
35
|
Four findings fail the run: an unresolved context citation, a banned character, word, or spelling, a skill folder carrying no `REQUIREMENT.md`, and a credential-shaped value in the tree the package ships. Each is a fact with no false-positive class behind it.
|
|
36
36
|
|
|
37
|
-
Three of the four are the ones `
|
|
37
|
+
Three of the four are the ones `aitk gate run` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the architecture record already ranks content leaving the repository above content that stays.
|
|
38
38
|
|
|
39
39
|
Everything else reports. A heavy bullet, a long entry, a board row nothing resolves, a degradation term in a comment, and an implementation reaching history ahead of its test are judgments a reader settles. A push failing on one of those teaches contributors to route around the stage, which is the split recorded across every audit here and the one this command inherits rather than moves.
|
|
40
40
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -64,6 +64,7 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
64
64
|
| `aitk census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
|
|
65
65
|
| `aitk audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`) |
|
|
66
66
|
| `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
|
|
67
|
+
| `aitk gate run` | Run every stage that guards a branch here, scoping shell, types, and tests to the changed set (`--all`, `--no-write`, `--nested`, `--json`) |
|
|
67
68
|
| `aitk inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`) |
|
|
68
69
|
| `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
|
|
69
70
|
| `aitk serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
|
|
@@ -116,6 +117,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
|
|
|
116
117
|
| `labels` | `audit` |
|
|
117
118
|
| `autoship` | `classify` |
|
|
118
119
|
| `audits` | `run`, `list` |
|
|
120
|
+
| `gate` | `run` |
|
|
119
121
|
|
|
120
122
|
Common patterns:
|
|
121
123
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Merge gate
|
|
3
|
+
description: Running the gate this repository verifies a branch with, what the stage table holds and what stays a script, how the changed set scopes three stages, and why a stage that cannot read its input reports rather than passing
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Merge gate
|
|
7
|
+
|
|
8
|
+
`aitk gate run` runs every stage that guards a branch here, in order, stopping at the first stage that finds a fact. `bun run check` and `bun run check:ci` resolve to it, and `scripts/core/update.sh` calls it with `--nested` after a dependency update.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
aitk gate run
|
|
12
|
+
aitk gate run --all --no-write
|
|
13
|
+
aitk gate run --json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Option | Behavior |
|
|
17
|
+
| ------------ | ----------------------------------------------------------------------------- |
|
|
18
|
+
| `--all` | Run every stage instead of scoping shell, types, and tests to the changed set |
|
|
19
|
+
| `--no-write` | Check formatting instead of applying it, which is what a merge gate wants |
|
|
20
|
+
| `--nested` | Suppress the outer frame when a calling script has already opened one |
|
|
21
|
+
| `--json` | Add a machine-readable record on stdout |
|
|
22
|
+
|
|
23
|
+
## What the command owns and what it runs
|
|
24
|
+
|
|
25
|
+
Three things sit in the command: the stage table in `src/gate/stages.ts`, the changed-file scoping and the run loop in `src/gate/sequencer.ts`, and every threshold comparison in `src/gate/measures.ts`. Each individual check is the script or the verb it already was, under `scripts/core/` or behind another `aitk` command, and the move changed none of them. Sequencing, scoping, and comparison are where the recurring defects were, and a check whose behavior changed while its sequencing moved would make any regression impossible to attribute.
|
|
26
|
+
|
|
27
|
+
A stage is a list of checks and a check is one of four kinds:
|
|
28
|
+
|
|
29
|
+
| Kind | What it is |
|
|
30
|
+
| --------- | ------------------------------------------------------------------------------- |
|
|
31
|
+
| `command` | Any binary, run from the project root |
|
|
32
|
+
| `cli` | This checkout's own `src/cli.ts`, never a globally installed `aitk` |
|
|
33
|
+
| `drift` | A regenerated pathspec asserted against the index and against the untracked set |
|
|
34
|
+
| `measure` | A reading whose verdict is a comparison rather than an exit code |
|
|
35
|
+
|
|
36
|
+
A `cli` check runs the source rather than the binary because a globally installed `aitk` resolves to the main checkout no matter which worktree is running, so a gate reading through it would measure the wrong tree and pass a branch it never opened.
|
|
37
|
+
|
|
38
|
+
Every check is an argument vector rather than a shell line, so no stage carries a quoting hazard and a `drift` pathspec reaches git exactly as the table spells it.
|
|
39
|
+
|
|
40
|
+
## Scoping
|
|
41
|
+
|
|
42
|
+
Shell, types, and tests read the changed set. Everything else always runs, because its input is diffuse enough that no path predicts it.
|
|
43
|
+
|
|
44
|
+
The changed set unions the branch diff against the merge base with `origin/main`, the working tree, and untracked files, which is what a pull request will contain. The baseline is the remote ref and not local `main`, since on `main` itself the local ref is HEAD and every unpushed commit would drop out. Every fallback widens rather than narrows: no merge base at all runs every stage, and a local baseline equal to HEAD does the same. `--all` turns scoping off outright, which is what `bun run check:ci` passes so CI stays the backstop for a wrong local scoping decision.
|
|
45
|
+
|
|
46
|
+
## A stage that cannot read its input
|
|
47
|
+
|
|
48
|
+
A stage reports one of four states. It passed, it was scoped out, it found a fact, or it could not measure its input at all.
|
|
49
|
+
|
|
50
|
+
The fourth is the one worth naming. An absent tool, a catalog that did not report, a corpus with nothing under it: each used to print a line that read like a pass. Now the run records it as unmeasured, the closing line says how many stages measured nothing, and the reader is not told a verdict nobody took.
|
|
51
|
+
|
|
52
|
+
What happens next depends on where the run is. On a contributor's machine it warns and the run still exits 0, because an absent tool there is somebody mid-setup. Under CI, read off `CI=true`, it refuses, because the same absence on a runner is a broken workflow step.
|
|
53
|
+
|
|
54
|
+
## Exit codes
|
|
55
|
+
|
|
56
|
+
| Code | Meaning |
|
|
57
|
+
| ---- | ------------------------------------------------------------- |
|
|
58
|
+
| `0` | every stage that ran reported, and none found a fact |
|
|
59
|
+
| `1` | a stage found a fact, or could not measure its input under CI |
|
|
60
|
+
|
|
61
|
+
One code for a failure, which is what a `bun run` caller and a git hook both read. An unmeasured stage takes no code of its own, since it has already refused under CI and reports on a contributor's machine, so a second code would name a state no caller branches on.
|
|
62
|
+
|
|
63
|
+
## The record
|
|
64
|
+
|
|
65
|
+
`--json` puts one record on stdout and keeps every diagnostic on stderr:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"ok": true,
|
|
70
|
+
"root": "/path/to/checkout",
|
|
71
|
+
"scoped": true,
|
|
72
|
+
"changed": 12,
|
|
73
|
+
"summary": {
|
|
74
|
+
"ran": 23,
|
|
75
|
+
"passed": 22,
|
|
76
|
+
"skipped": 1,
|
|
77
|
+
"unmeasured": 0,
|
|
78
|
+
"failed": 0
|
|
79
|
+
},
|
|
80
|
+
"stages": [{ "id": "indexes", "label": "Indexes", "status": "passed" }]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A failing stage carries its remedy in `failure`, and the same line goes to stderr so a caller reading neither the record nor the frame is still told what to fix.
|
package/docs/agents/index.md
CHANGED
|
@@ -18,6 +18,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
18
18
|
- [Self-stated counts](counts.md): Reading a sentence that asserts a closed catalog's size, how a match is decided, the plausibility filter that keeps a generic word from matching a subset, and why the sweep reports rather than gates
|
|
19
19
|
- [Demo](demo.md): Compiling a screencast draft into a runnable plan, driving a served application to a recording and a still, the pointer the recording paints, and what each refusal reports
|
|
20
20
|
- [Docs](docs.md): How aitk docs resolves the toolkit's own reference surface from an install root, and how a split domain is named
|
|
21
|
+
- [Merge gate](gate.md): Running the gate this repository verifies a branch with, what the stage table holds and what stays a script, how the changed set scopes three stages, and why a stage that cannot read its input reports rather than passing
|
|
21
22
|
- [Indexes](indexes.md): Flags, exit codes, and JSON shape for aitk indexes regen, plus when it auto-stages what it rewrote
|
|
22
23
|
- [Install and sync](install-and-sync.md): What each install and sync verb writes, refuses, or leaves alone, and how drift is attributed in a target project
|
|
23
24
|
- [Intake](intake.md): Reading intake folder counts and items, the three read states an item can be in, landing a batch of selections in one cluster, the refusal reasons, and why a call is scoped to one file
|
|
@@ -126,9 +126,9 @@ The condition on that was something identifying a finite verb rather than guessi
|
|
|
126
126
|
|
|
127
127
|
Exit codes are `0` for a completed run with no gating finding, `1` for a refusal, `2` for a ban hit, and `3` for a shipped ban set that arrived empty. A banned character, word, or spelling fails the run. Bullet, paragraph, and depth weight are judgments a reader settles, and cadence is a distribution whose healthy range moves with the surface, so all four report under every code.
|
|
128
128
|
|
|
129
|
-
`3` is separate from `1` because the two want different responses from a caller. A refusal means no corpus was built, and the `Markdown bans` stage in `
|
|
129
|
+
`3` is separate from `1` because the two want different responses from a caller. A refusal means no corpus was built, and the `Markdown bans` stage in `aitk gate run` is right to report it as unmeasured rather than as a pass. An empty set means the corpus was walked and nothing was looked for, so that stage fails the push on `3` rather than skipping.
|
|
130
130
|
|
|
131
|
-
`2` rather than `1` for the gate keeps a measurement that succeeded and found something distinct from the audit declining to measure at all. A caller reading one as the other sends a reader hunting a defect that does not exist, which is the distinction `aitk context audit` and the
|
|
131
|
+
`2` rather than `1` for the gate keeps a measurement that succeeded and found something distinct from the audit declining to measure at all. A caller reading one as the other sends a reader hunting a defect that does not exist, which is the distinction `aitk context audit` and the gate's own seed stage already draw between the same two codes.
|
|
132
132
|
|
|
133
133
|
A banned character is a fact rather than a judgment, which is the test that admits it to a gate. What held it back was that gating on day one against a corpus never checked mechanically fails loudly on work nobody has had a chance to fix. The order was to land the verb reporting, measure the corpus once, fix what it finds, and turn the gate on as its own change, and the gate is the last of the four.
|
|
134
134
|
|
|
@@ -148,7 +148,7 @@ A hit the closed set cannot separate from correct prose is the case with no thir
|
|
|
148
148
|
|
|
149
149
|
### Where the rules are enforced
|
|
150
150
|
|
|
151
|
-
Four surfaces apply the ban sets and three of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, and the `Markdown bans` stage in `
|
|
151
|
+
Four surfaces apply the ban sets and three of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, and the `Markdown bans` stage in `aitk gate run` runs it across the whole corpus before a push. Each hook parsed its own copy of the word bans in awk before that, which left a British spelling passing at edit time and failing the push with nothing in between explaining the difference.
|
|
152
152
|
|
|
153
153
|
The seed copy moved onto the verb when the sets became data, since its awk had nothing left to parse. It resolves one runner where the toolkit copy resolves two, looking for no checkout source, and a machine carrying no `aitk` gets a report naming the binary to install rather than a silent pass. `scripts/core/check-seed-independence.sh` scopes its walk to markdown and leaves the seed hooks outside it, which its own comment records as deliberate.
|
|
154
154
|
|
|
@@ -59,7 +59,7 @@ The exemption travels with the line rather than sitting in a path list away from
|
|
|
59
59
|
|
|
60
60
|
Exit codes are `0` when the shipped tree carries no credential-shaped value, `1` for a refusal, and `2` for at least one value found.
|
|
61
61
|
|
|
62
|
-
This is the one entry in `aitk audits run` that gates without
|
|
62
|
+
This is the one entry in `aitk audits run` that gates without an `aitk gate run` stage behind it. A credential in the published tree is a fact rather than a judgment, which is the test the catalog asks any gating addition to pass, and the architecture record already ranks content leaving the repository above content that stays.
|
|
63
63
|
|
|
64
64
|
A refusal is never a clean tree. Five reasons produce one, and each exits `1`, because zero findings over zero files reads in the report exactly like zero findings over the whole shipped tree.
|
|
65
65
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erclx/aitk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.52.
|
|
4
|
+
"version": "3.52.1",
|
|
5
5
|
"description": "Infrastructure and quality tooling for developer workflows",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"check:install": "./scripts/core/install-check.sh",
|
|
35
35
|
"test": "bun --bun vitest run",
|
|
36
36
|
"prepare": "husky",
|
|
37
|
-
"check": "
|
|
38
|
-
"check:ci": "
|
|
37
|
+
"check": "bun src/cli.ts gate run",
|
|
38
|
+
"check:ci": "bun src/cli.ts gate run --all --no-write",
|
|
39
39
|
"update": "./scripts/core/update.sh",
|
|
40
40
|
"clean": "./scripts/core/clean.sh",
|
|
41
41
|
"snapshot": "./scripts/core/snapshot.sh",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Prints every seed root carrying a `.claude/`, one per line and relative to
|
|
3
|
+
# the project root.
|
|
4
|
+
#
|
|
5
|
+
# The discovery rule has one definition, `collect_seed_roots` in
|
|
6
|
+
# `scripts/lib/tooling.sh`, which `check-seed-independence.sh` already reads.
|
|
7
|
+
# This file is the route a caller outside bash takes to that same answer, so a
|
|
8
|
+
# stack seeding `.claude/` later reaches both readers and the two stages
|
|
9
|
+
# measuring seed content cannot disagree about which roots exist.
|
|
10
|
+
set -e
|
|
11
|
+
set -o pipefail
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
15
|
+
|
|
16
|
+
source "$PROJECT_ROOT/scripts/lib/tooling.sh"
|
|
17
|
+
|
|
18
|
+
collect_seed_roots
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Repairs `core.bare`, which Claude Code's worktree entry leaves set in the
|
|
3
|
+
# shared config with nothing restoring it. The flag strands the main worktree
|
|
4
|
+
# and breaks the git reads that scope a verification run, so this runs ahead of
|
|
5
|
+
# every stage rather than as one of them.
|
|
6
|
+
#
|
|
7
|
+
# The rule itself lives in `scripts/lib/worktree.sh`, the one bash function in
|
|
8
|
+
# this repository under test, so this file is the invocation and never a second
|
|
9
|
+
# copy of the guard that spares a genuinely bare repository.
|
|
10
|
+
set -e
|
|
11
|
+
set -o pipefail
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
15
|
+
|
|
16
|
+
source "$PROJECT_ROOT/scripts/lib/ui.sh"
|
|
17
|
+
source "$PROJECT_ROOT/scripts/lib/worktree.sh"
|
|
18
|
+
|
|
19
|
+
repair_bare_flag "$PROJECT_ROOT"
|
package/scripts/core/update.sh
CHANGED
|
@@ -21,11 +21,11 @@ main() {
|
|
|
21
21
|
bun update --interactive
|
|
22
22
|
|
|
23
23
|
log_step "Verifying project health"
|
|
24
|
-
if [ -f "$
|
|
25
|
-
|
|
24
|
+
if [ -f "$PROJECT_ROOT/src/cli.ts" ]; then
|
|
25
|
+
bun "$PROJECT_ROOT/src/cli.ts" gate run --nested
|
|
26
26
|
log_info "All checks passed"
|
|
27
27
|
else
|
|
28
|
-
log_warn "Verification
|
|
28
|
+
log_warn "Verification entry point not found, skipping."
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
31
|
echo -e "${GREY}└${NC}\n"
|
package/src/audits/catalog.ts
CHANGED
|
@@ -469,8 +469,8 @@ const RECORD_KINDS: readonly (readonly [string, Corpus])[] = [
|
|
|
469
469
|
/**
|
|
470
470
|
* Every audit the aggregate runs.
|
|
471
471
|
*
|
|
472
|
-
* `context`, `markdown`, and `skills` gate because
|
|
473
|
-
* already fails a push on each. `secrets` is the one entry that gates without
|
|
472
|
+
* `context`, `markdown`, and `skills` gate because the merge gate in
|
|
473
|
+
* `src/gate/stages.ts` already fails a push on each. `secrets` is the one entry that gates without
|
|
474
474
|
* a stage behind it, added deliberately rather than as a side effect, since a
|
|
475
475
|
* credential in the published tree is a fact and the split this repository
|
|
476
476
|
* records gates a fact and reports a judgment. Weigh any further addition
|
package/src/audits/run.ts
CHANGED
|
@@ -32,7 +32,8 @@ export type Spawn = (spec: AuditSpec) => Promise<SpawnResult>
|
|
|
32
32
|
* Runs each verb out of the checkout this CLI is executing from.
|
|
33
33
|
*
|
|
34
34
|
* `process.execPath` and the resolved `cli.ts` rather than a bare `aitk`, for
|
|
35
|
-
* the reason `
|
|
35
|
+
* the reason `cliRunner` in `src/gate/sequencer.ts` already names: a globally
|
|
36
|
+
* installed binary resolves to
|
|
36
37
|
* the main checkout no matter which worktree is running, so the aggregate would
|
|
37
38
|
* measure the wrong tree and report a pass over a branch it never read.
|
|
38
39
|
*
|
package/src/cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { register as records } from '@/commands/records'
|
|
|
30
30
|
import { register as sessions } from '@/commands/sessions'
|
|
31
31
|
import { register as worktrees } from '@/commands/worktrees'
|
|
32
32
|
import { register as audits } from '@/commands/audits'
|
|
33
|
+
import { register as gate } from '@/commands/gate'
|
|
33
34
|
import { register as secrets } from '@/commands/secrets'
|
|
34
35
|
import { register as deps } from '@/commands/deps'
|
|
35
36
|
import { register as labels } from '@/commands/labels'
|
|
@@ -84,6 +85,7 @@ function showHelp(): void {
|
|
|
84
85
|
`${GREY}│${NC} autoship [cmd] ${GREY}# Decide whether a changed set needs the review pass (classify)${NC}`,
|
|
85
86
|
`${GREY}│${NC} census [path] ${GREY}# Report tracked file count, extension breakdown, and line totals${NC}`,
|
|
86
87
|
`${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
|
|
88
|
+
`${GREY}│${NC} gate [cmd] ${GREY}# Run the merge gate stage by stage (run)${NC}`,
|
|
87
89
|
`${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
|
|
88
90
|
`${GREY}│${NC}`,
|
|
89
91
|
`${GREY}│${NC} ${WHITE}Sandbox:${NC}`,
|
|
@@ -132,6 +134,7 @@ function showHelp(): void {
|
|
|
132
134
|
`${GREY}│${NC} aitk labels audit --json`,
|
|
133
135
|
`${GREY}│${NC} aitk census --json`,
|
|
134
136
|
`${GREY}│${NC} aitk audits run --json`,
|
|
137
|
+
`${GREY}│${NC} aitk gate run --all --no-write`,
|
|
135
138
|
`${GREY}│${NC} aitk upgrade --json`,
|
|
136
139
|
`${GREY}└${NC}`,
|
|
137
140
|
]
|
|
@@ -186,6 +189,7 @@ labels(program)
|
|
|
186
189
|
autoship(program)
|
|
187
190
|
census(program)
|
|
188
191
|
audits(program)
|
|
192
|
+
gate(program)
|
|
189
193
|
upgrade(program)
|
|
190
194
|
|
|
191
195
|
program.parse()
|
package/src/commands/claude.ts
CHANGED
|
@@ -1136,8 +1136,8 @@ function refuseAudit(
|
|
|
1136
1136
|
/**
|
|
1137
1137
|
* Prints nothing when every skill carries a requirement.
|
|
1138
1138
|
*
|
|
1139
|
-
* `--requirements-only` is what
|
|
1140
|
-
*
|
|
1139
|
+
* `--requirements-only` is what the merge gate runs on every push, and it pipes
|
|
1140
|
+
* a stage's whole output into its own frame. A passing gate that printed
|
|
1141
1141
|
* its frame would nest one inside the other on every contributor's push.
|
|
1142
1142
|
*/
|
|
1143
1143
|
function reportRequirementGate(report: SkillsAudit): void {
|
package/src/commands/context.ts
CHANGED
|
@@ -349,8 +349,8 @@ function refuse(
|
|
|
349
349
|
/**
|
|
350
350
|
* Prints nothing when every path resolves.
|
|
351
351
|
*
|
|
352
|
-
* `--citations-only` is what
|
|
353
|
-
*
|
|
352
|
+
* `--citations-only` is what the merge gate runs on every push, and it pipes
|
|
353
|
+
* a stage's whole output into its own frame. A passing gate that printed
|
|
354
354
|
* its frame would nest one inside the other on every contributor's push.
|
|
355
355
|
*/
|
|
356
356
|
function reportGate(report: ScannedCitations): void {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import {
|
|
3
|
+
cliRunner,
|
|
4
|
+
collectChangedFiles,
|
|
5
|
+
commandRunner,
|
|
6
|
+
exitCodeFor,
|
|
7
|
+
type GateContext,
|
|
8
|
+
repairBareFlag,
|
|
9
|
+
type StageResult,
|
|
10
|
+
type Summary,
|
|
11
|
+
runStages,
|
|
12
|
+
summarize,
|
|
13
|
+
} from '@/gate/sequencer'
|
|
14
|
+
import { STAGES } from '@/gate/stages'
|
|
15
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
16
|
+
import {
|
|
17
|
+
intro,
|
|
18
|
+
logError,
|
|
19
|
+
logInfo,
|
|
20
|
+
logStep,
|
|
21
|
+
logWarn,
|
|
22
|
+
outro,
|
|
23
|
+
palette,
|
|
24
|
+
pipeOutput,
|
|
25
|
+
plural,
|
|
26
|
+
} from '@/ui'
|
|
27
|
+
|
|
28
|
+
interface RunCommandOptions {
|
|
29
|
+
readonly all?: boolean
|
|
30
|
+
readonly write?: boolean
|
|
31
|
+
readonly nested?: boolean
|
|
32
|
+
readonly json?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function register(program: Command): void {
|
|
36
|
+
const gate = program
|
|
37
|
+
.command('gate')
|
|
38
|
+
.description('Run the merge gate this repository verifies a branch with')
|
|
39
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
40
|
+
|
|
41
|
+
gate
|
|
42
|
+
.command('run')
|
|
43
|
+
.description(
|
|
44
|
+
'Run every gating stage in order, scoping shell, types, and tests to the changed set',
|
|
45
|
+
)
|
|
46
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
47
|
+
.option(
|
|
48
|
+
'--all',
|
|
49
|
+
'Run every stage instead of scoping shell, types, and tests to changed files',
|
|
50
|
+
)
|
|
51
|
+
.option(
|
|
52
|
+
'--no-write',
|
|
53
|
+
'Check formatting instead of applying it, which is what a merge gate wants',
|
|
54
|
+
)
|
|
55
|
+
.option(
|
|
56
|
+
'--nested',
|
|
57
|
+
'Suppress the outer frame when another script opened one',
|
|
58
|
+
)
|
|
59
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
60
|
+
.addHelpText(
|
|
61
|
+
'after',
|
|
62
|
+
[
|
|
63
|
+
'',
|
|
64
|
+
'Exit codes:',
|
|
65
|
+
' 0 every stage that ran reported, and none found a fact',
|
|
66
|
+
' 1 a stage found a fact, or could not measure its input under CI',
|
|
67
|
+
'',
|
|
68
|
+
'A stage halts the run, so clearing a regenerate-then-assert stage',
|
|
69
|
+
'reveals the next one behind it rather than the whole set at once.',
|
|
70
|
+
'',
|
|
71
|
+
'A stage that cannot read its input reports rather than passing. On a',
|
|
72
|
+
"contributor's machine that is a warning and the run still exits 0,",
|
|
73
|
+
'because an absent tool there is somebody mid-setup. Under CI it',
|
|
74
|
+
'refuses, because the same absence is a broken workflow step and a',
|
|
75
|
+
'green run over a stage that measured nothing is the pass the gate',
|
|
76
|
+
'exists to withhold.',
|
|
77
|
+
'',
|
|
78
|
+
'Examples:',
|
|
79
|
+
' aitk gate run',
|
|
80
|
+
' aitk gate run --all --no-write',
|
|
81
|
+
' aitk gate run --json',
|
|
82
|
+
'',
|
|
83
|
+
].join('\n'),
|
|
84
|
+
)
|
|
85
|
+
.action(async (opts: RunCommandOptions) => {
|
|
86
|
+
process.exitCode = await runGate(opts)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runGate(opts: RunCommandOptions): Promise<number> {
|
|
91
|
+
const root = PROJECT_ROOT
|
|
92
|
+
const emitJson = opts.json ?? false
|
|
93
|
+
const nested = opts.nested ?? false
|
|
94
|
+
const write = opts.write ?? true
|
|
95
|
+
const run = commandRunner(root)
|
|
96
|
+
|
|
97
|
+
if (!emitJson && !nested) intro('aitk gate run')
|
|
98
|
+
|
|
99
|
+
await repairBareFlag(root)
|
|
100
|
+
|
|
101
|
+
const changed = opts.all
|
|
102
|
+
? { scoped: false, files: [] }
|
|
103
|
+
: await collectChangedFiles(run)
|
|
104
|
+
if (!emitJson && changed.notice !== undefined) logWarn(changed.notice)
|
|
105
|
+
|
|
106
|
+
const ctx: GateContext = {
|
|
107
|
+
root,
|
|
108
|
+
ci: process.env.CI === 'true',
|
|
109
|
+
run,
|
|
110
|
+
cli: cliRunner(root),
|
|
111
|
+
write,
|
|
112
|
+
changed: changed.scoped ? changed.files : undefined,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const results = await runStages(
|
|
116
|
+
STAGES,
|
|
117
|
+
ctx,
|
|
118
|
+
emitJson ? undefined : (result) => report(result),
|
|
119
|
+
)
|
|
120
|
+
const summary = summarize(results)
|
|
121
|
+
const code = exitCodeFor(results)
|
|
122
|
+
|
|
123
|
+
if (emitJson) {
|
|
124
|
+
const failed = results.find((result) => result.status === 'failed')
|
|
125
|
+
// Diagnostics reach stderr in every mode, so a caller reading the record
|
|
126
|
+
// alone is not the only one told what went wrong.
|
|
127
|
+
if (failed?.failure !== undefined) {
|
|
128
|
+
process.stderr.write(`${failed.label}: ${failed.failure}\n`)
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(
|
|
131
|
+
`${JSON.stringify({
|
|
132
|
+
ok: code === 0,
|
|
133
|
+
root,
|
|
134
|
+
scoped: changed.scoped,
|
|
135
|
+
changed: changed.files.length,
|
|
136
|
+
summary,
|
|
137
|
+
stages: results.map(({ id, label, status, failure }) => ({
|
|
138
|
+
id,
|
|
139
|
+
label,
|
|
140
|
+
status,
|
|
141
|
+
failure,
|
|
142
|
+
})),
|
|
143
|
+
})}\n`,
|
|
144
|
+
)
|
|
145
|
+
return code
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!nested) close(summary, code)
|
|
149
|
+
return code
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function report(result: StageResult): void {
|
|
153
|
+
logStep(result.label)
|
|
154
|
+
for (const emission of result.emissions) {
|
|
155
|
+
if (emission.kind === 'info') logInfo(emission.text)
|
|
156
|
+
else if (emission.kind === 'warn') logWarn(emission.text)
|
|
157
|
+
else pipeOutput(emission.text)
|
|
158
|
+
}
|
|
159
|
+
if (result.failure !== undefined) logError(result.failure)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The closing verdict, which names what the run did not measure rather than
|
|
164
|
+
* reporting a bare pass over it.
|
|
165
|
+
*
|
|
166
|
+
* A run where every stage read its input still closes on the line the script
|
|
167
|
+
* this replaces closed on, so nothing about the ordinary case moved. A run
|
|
168
|
+
* carrying an unmeasured stage says so, because a green line over a stage that
|
|
169
|
+
* looked at nothing is exactly the silence the reporting outcome exists
|
|
170
|
+
* against.
|
|
171
|
+
*/
|
|
172
|
+
function close(summary: Summary, code: number): void {
|
|
173
|
+
const { GREEN, NC, RED, YELLOW } = palette(process.stderr)
|
|
174
|
+
outro()
|
|
175
|
+
|
|
176
|
+
if (code !== 0) {
|
|
177
|
+
process.stderr.write(`${RED}✗ Verification failed${NC}\n\n`)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (summary.unmeasured > 0) {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`${YELLOW}! Verification passed, ${plural(summary.unmeasured, 'stage')} measured nothing${NC}\n\n`,
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process.stderr.write(`${GREEN}✓ Verification passed${NC}\n\n`)
|
|
189
|
+
}
|