its-magic 0.1.2-32 → 0.1.2-33
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/README.md +59 -11
- package/installer.ps1 +1 -0
- package/installer.py +19 -0
- package/installer.sh +1 -1
- package/package.json +1 -1
- package/template/.cursor/commands/execute.md +8 -1
- package/template/.cursor/rules/quality.mdc +1 -1
- package/template/.cursor/scratchpad.local.example.md +6 -0
- package/template/.cursor/scratchpad.md +6 -0
- package/template/.github/workflows/ci.yml +1 -1
- package/template/README.md +59 -11
- package/template/docs/developer/README.md +44 -0
- package/template/docs/engineering/context/installer-owned-paths.manifest +7 -0
- package/template/docs/engineering/runbook.md +63 -0
- package/template/scripts/doc_profile_lib.py +415 -0
- package/template/scripts/sync_push_gates.py +198 -0
- package/template/scripts/validate-and-push.ps1 +156 -56
- package/template/scripts/validate-and-push.sh +140 -60
- package/template/scripts/validate_doc_profile.py +103 -0
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
# Part of the its-magic quality chain:
|
|
5
5
|
# Cursor AI loop → validate-and-push → CI auto-fix (GitHub)
|
|
6
6
|
#
|
|
7
|
-
# Reads
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# only when everything passes.
|
|
7
|
+
# Reads merged scratchpad (installer merge / DEC-0055) for sync policy and
|
|
8
|
+
# runbook keys for commands. Opt-in push only when policy allows; reason codes
|
|
9
|
+
# on stdout/stderr. See docs/engineering/runbook.md (executable wiring: DEC-0058).
|
|
11
10
|
# -------------------------------------------------------------------
|
|
12
11
|
|
|
13
12
|
Param(
|
|
14
13
|
[int]$MaxAttempts = 5,
|
|
15
14
|
[string]$Branch,
|
|
16
|
-
[switch]$NoCommit
|
|
15
|
+
[switch]$NoCommit,
|
|
16
|
+
[switch]$DryRun
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
$ErrorActionPreference = "Stop"
|
|
@@ -24,7 +24,38 @@ function Log-Pass { param($Msg) Write-Host "[pass] $Msg" -ForegroundColor Gree
|
|
|
24
24
|
function Log-Fail { param($Msg) Write-Host "[fail] $Msg" -ForegroundColor Red }
|
|
25
25
|
function Log-Warn { param($Msg) Write-Host "[warn] $Msg" -ForegroundColor Yellow }
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
function Resolve-PythonExe {
|
|
28
|
+
foreach ($name in @("python", "python3")) {
|
|
29
|
+
$cmd = Get-Command $name -ErrorAction SilentlyContinue
|
|
30
|
+
if ($cmd) { return $cmd.Source }
|
|
31
|
+
}
|
|
32
|
+
return $null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Invoke-GateJson {
|
|
36
|
+
param(
|
|
37
|
+
[string]$PythonExe,
|
|
38
|
+
[string]$GateScript,
|
|
39
|
+
[string]$SubCommand,
|
|
40
|
+
[string]$RepoRoot,
|
|
41
|
+
[string]$BranchName
|
|
42
|
+
)
|
|
43
|
+
$outFile = [System.IO.Path]::GetTempFileName()
|
|
44
|
+
$errFile = [System.IO.Path]::GetTempFileName()
|
|
45
|
+
try {
|
|
46
|
+
$p = Start-Process -FilePath $PythonExe `
|
|
47
|
+
-ArgumentList @($GateScript, $SubCommand, "--root", $RepoRoot, "--branch", $BranchName) `
|
|
48
|
+
-Wait -PassThru -NoNewWindow `
|
|
49
|
+
-RedirectStandardOutput $outFile -RedirectStandardError $errFile
|
|
50
|
+
$stderr = if (Test-Path $errFile) { Get-Content -Raw -Path $errFile } else { "" }
|
|
51
|
+
$stdout = if (Test-Path $outFile) { Get-Content -Raw -Path $outFile } else { "" }
|
|
52
|
+
if ($stderr.Trim().Length -gt 0) { Write-Host $stderr.TrimEnd() }
|
|
53
|
+
return @{ ExitCode = $p.ExitCode; StdOut = $stdout.Trim() }
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
Remove-Item -Path $outFile, $errFile -ErrorAction SilentlyContinue
|
|
57
|
+
}
|
|
58
|
+
}
|
|
28
59
|
|
|
29
60
|
function Read-RunbookKey {
|
|
30
61
|
param([string]$Key)
|
|
@@ -38,16 +69,16 @@ function Read-RunbookKey {
|
|
|
38
69
|
return $val
|
|
39
70
|
}
|
|
40
71
|
|
|
41
|
-
$
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
$py = Resolve-PythonExe
|
|
73
|
+
if (-not $py) {
|
|
74
|
+
Log-Fail "reason_code=PYTHON_NOT_ON_PATH"
|
|
75
|
+
Log-Warn "Install Python 3 and ensure it is on PATH for merged scratchpad policy gates."
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
46
78
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Log-
|
|
50
|
-
Log-Warn "Fill in the runbook first, then re-run."
|
|
79
|
+
$gateScript = Join-Path $root "scripts\sync_push_gates.py"
|
|
80
|
+
if (-not (Test-Path $gateScript -PathType Leaf)) {
|
|
81
|
+
Log-Fail "reason_code=SYNC_GATE_SCRIPT_MISSING"
|
|
51
82
|
exit 1
|
|
52
83
|
}
|
|
53
84
|
|
|
@@ -58,22 +89,75 @@ if (-not $Branch) {
|
|
|
58
89
|
|
|
59
90
|
Log-Info "validate-and-push loop"
|
|
60
91
|
Log-Info "Branch: $Branch | Max attempts: $MaxAttempts"
|
|
92
|
+
if ($DryRun) { Log-Info "Dry-run: no git push will run." }
|
|
93
|
+
Write-Host ""
|
|
94
|
+
|
|
95
|
+
$pol = Invoke-GateJson -PythonExe $py -GateScript $gateScript -SubCommand "policy" -RepoRoot $root -BranchName $Branch
|
|
96
|
+
try {
|
|
97
|
+
$polObj = $pol.StdOut | ConvertFrom-Json
|
|
98
|
+
} catch {
|
|
99
|
+
Log-Fail "reason_code=SYNC_POLICY_PARSE_FAILED"
|
|
100
|
+
exit 1
|
|
101
|
+
}
|
|
102
|
+
if (-not $polObj.ok) {
|
|
103
|
+
Log-Fail ("reason_code=" + $polObj.reason_code)
|
|
104
|
+
exit 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$TestCmd = Read-RunbookKey "TEST_COMMAND"
|
|
108
|
+
$LintCmd = Read-RunbookKey "LINT_COMMAND"
|
|
109
|
+
$TypecheckCmd = Read-RunbookKey "TYPECHECK_COMMAND"
|
|
110
|
+
$LintFixCmd = Read-RunbookKey "LINT_FIX_COMMAND"
|
|
111
|
+
$FormatCmd = Read-RunbookKey "FORMAT_COMMAND"
|
|
112
|
+
$timeoutRaw = Read-RunbookKey "TEST_TIMEOUT_SECONDS"
|
|
113
|
+
$TestTimeoutSec = 120
|
|
114
|
+
if ($timeoutRaw -match '^\d+$') { $TestTimeoutSec = [int]$timeoutRaw }
|
|
115
|
+
|
|
116
|
+
if (-not $TestCmd) {
|
|
117
|
+
Log-Fail "reason_code=TEST_COMMAND_MISSING"
|
|
118
|
+
Log-Warn "TEST_COMMAND is required by sync policy for push-eligible paths."
|
|
119
|
+
exit 1
|
|
120
|
+
}
|
|
121
|
+
|
|
61
122
|
if ($TestCmd) { Log-Info "TEST_COMMAND: $TestCmd" }
|
|
62
123
|
if ($LintCmd) { Log-Info "LINT_COMMAND: $LintCmd" }
|
|
63
124
|
if ($TypecheckCmd) { Log-Info "TYPECHECK_COMMAND: $TypecheckCmd" }
|
|
64
125
|
if ($LintFixCmd) { Log-Info "LINT_FIX_COMMAND: $LintFixCmd" }
|
|
65
126
|
if ($FormatCmd) { Log-Info "FORMAT_COMMAND: $FormatCmd" }
|
|
127
|
+
Log-Info "TEST_TIMEOUT_SECONDS: $TestTimeoutSec"
|
|
66
128
|
Write-Host ""
|
|
67
129
|
|
|
68
130
|
function Run-Cmd {
|
|
69
|
-
param([string]$Cmd)
|
|
131
|
+
param([string]$Cmd, [int]$TimeoutSec = 0)
|
|
70
132
|
$prev = $ErrorActionPreference
|
|
71
133
|
$ErrorActionPreference = "Continue"
|
|
72
134
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
135
|
+
if ($TimeoutSec -le 0) {
|
|
136
|
+
Invoke-Expression $Cmd
|
|
137
|
+
return @{ Ok = ($LASTEXITCODE -eq 0); TimedOut = $false }
|
|
138
|
+
}
|
|
139
|
+
$job = Start-Job -ScriptBlock {
|
|
140
|
+
param($c, $r)
|
|
141
|
+
Set-Location $r
|
|
142
|
+
try {
|
|
143
|
+
Invoke-Expression $c | Out-Null
|
|
144
|
+
} catch {
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
if ($null -eq $LASTEXITCODE) { return 0 }
|
|
148
|
+
return $LASTEXITCODE
|
|
149
|
+
} -ArgumentList $Cmd, $root
|
|
150
|
+
$finished = Wait-Job -Job $job -Timeout $TimeoutSec
|
|
151
|
+
if (-not $finished -or $job.State -eq "Running") {
|
|
152
|
+
Stop-Job -Job $job -ErrorAction SilentlyContinue
|
|
153
|
+
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
|
|
154
|
+
return @{ Ok = $false; TimedOut = $true }
|
|
155
|
+
}
|
|
156
|
+
$code = Receive-Job -Job $job
|
|
157
|
+
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
|
|
158
|
+
$exitNum = 1
|
|
159
|
+
if ($null -ne $code) { $exitNum = [int]$code[-1] }
|
|
160
|
+
return @{ Ok = ($exitNum -eq 0); TimedOut = $false }
|
|
77
161
|
} finally {
|
|
78
162
|
$ErrorActionPreference = $prev
|
|
79
163
|
}
|
|
@@ -86,51 +170,49 @@ for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
|
|
|
86
170
|
Push-Location $root
|
|
87
171
|
$allOk = $true
|
|
88
172
|
|
|
89
|
-
# 1. Run formatter if available
|
|
90
173
|
if ($FormatCmd) {
|
|
91
174
|
Log-Info "Running formatter..."
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
} else {
|
|
95
|
-
Log-Warn "Formatter reported issues (non-blocking)"
|
|
96
|
-
}
|
|
175
|
+
$r = Run-Cmd -Cmd $FormatCmd -TimeoutSec 0
|
|
176
|
+
if ($r.Ok) { Log-Pass "Format OK" } else { Log-Warn "Formatter reported issues (non-blocking)" }
|
|
97
177
|
}
|
|
98
178
|
|
|
99
|
-
# 2. Try lint fix if available
|
|
100
179
|
if ($LintFixCmd) {
|
|
101
180
|
Log-Info "Running lint auto-fix..."
|
|
102
|
-
Run-Cmd $LintFixCmd | Out-Null
|
|
181
|
+
Run-Cmd -Cmd $LintFixCmd -TimeoutSec 0 | Out-Null
|
|
103
182
|
}
|
|
104
183
|
|
|
105
|
-
# 3. Run lint check
|
|
106
184
|
if ($LintCmd) {
|
|
107
185
|
Log-Info "Running lint check..."
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
186
|
+
$r = Run-Cmd -Cmd $LintCmd -TimeoutSec $TestTimeoutSec
|
|
187
|
+
if ($r.TimedOut) {
|
|
188
|
+
Log-Fail "reason_code=OPTIONAL_CHECK_FAILED"
|
|
189
|
+
$allOk = $false
|
|
190
|
+
} elseif ($r.Ok) { Log-Pass "Lint OK" } else {
|
|
191
|
+
Log-Fail "reason_code=OPTIONAL_CHECK_FAILED"
|
|
112
192
|
$allOk = $false
|
|
113
193
|
}
|
|
114
194
|
}
|
|
115
195
|
|
|
116
|
-
# 4. Run typecheck (optional)
|
|
117
196
|
if ($TypecheckCmd) {
|
|
118
197
|
Log-Info "Running typecheck..."
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
198
|
+
$r = Run-Cmd -Cmd $TypecheckCmd -TimeoutSec $TestTimeoutSec
|
|
199
|
+
if ($r.TimedOut) {
|
|
200
|
+
Log-Fail "reason_code=OPTIONAL_CHECK_FAILED"
|
|
201
|
+
$allOk = $false
|
|
202
|
+
} elseif ($r.Ok) { Log-Pass "Typecheck OK" } else {
|
|
203
|
+
Log-Fail "reason_code=OPTIONAL_CHECK_FAILED"
|
|
123
204
|
$allOk = $false
|
|
124
205
|
}
|
|
125
206
|
}
|
|
126
207
|
|
|
127
|
-
# 5. Run tests
|
|
128
208
|
if ($TestCmd) {
|
|
129
209
|
Log-Info "Running tests..."
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
210
|
+
$r = Run-Cmd -Cmd $TestCmd -TimeoutSec $TestTimeoutSec
|
|
211
|
+
if ($r.TimedOut) {
|
|
212
|
+
Log-Fail "reason_code=TEST_TIMEOUT"
|
|
213
|
+
$allOk = $false
|
|
214
|
+
} elseif ($r.Ok) { Log-Pass "Tests OK" } else {
|
|
215
|
+
Log-Fail "reason_code=TEST_FAILED"
|
|
134
216
|
$allOk = $false
|
|
135
217
|
}
|
|
136
218
|
}
|
|
@@ -158,23 +240,41 @@ for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
|
|
|
158
240
|
|
|
159
241
|
if (-not $passed) { exit 1 }
|
|
160
242
|
|
|
243
|
+
$post = Invoke-GateJson -PythonExe $py -GateScript $gateScript -SubCommand "post" -RepoRoot $root -BranchName $Branch
|
|
244
|
+
try {
|
|
245
|
+
$postObj = $post.StdOut | ConvertFrom-Json
|
|
246
|
+
} catch {
|
|
247
|
+
Log-Fail "reason_code=SYNC_POST_PARSE_FAILED"
|
|
248
|
+
exit 1
|
|
249
|
+
}
|
|
250
|
+
if (-not $postObj.ok) {
|
|
251
|
+
Log-Fail ("reason_code=" + $postObj.reason_code)
|
|
252
|
+
exit 1
|
|
253
|
+
}
|
|
254
|
+
|
|
161
255
|
Write-Host ""
|
|
162
256
|
|
|
163
|
-
if (-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if ($status) {
|
|
167
|
-
Log-Info "Staging and committing changes..."
|
|
168
|
-
git add -A
|
|
169
|
-
git commit -m "fix: address check failures (validate-and-push)"
|
|
257
|
+
if ($DryRun -or $NoCommit) {
|
|
258
|
+
if ($DryRun) {
|
|
259
|
+
Log-Pass "reason_code=SYNC_PUSHED (dry-run; no git push)"
|
|
170
260
|
} else {
|
|
171
|
-
Log-Info "
|
|
261
|
+
Log-Info "Auto-commit disabled (--NoCommit). Push manually when ready."
|
|
262
|
+
Log-Pass "reason_code=SYNC_PUSHED (checks only; push skipped by flag)"
|
|
172
263
|
}
|
|
264
|
+
exit 0
|
|
265
|
+
}
|
|
173
266
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
267
|
+
Push-Location $root
|
|
268
|
+
$status = git status --porcelain
|
|
269
|
+
if ($status) {
|
|
270
|
+
Log-Info "Staging and committing changes..."
|
|
271
|
+
git add -A
|
|
272
|
+
git commit -m "fix: address check failures (validate-and-push)"
|
|
178
273
|
} else {
|
|
179
|
-
Log-Info "
|
|
274
|
+
Log-Info "Working tree clean, nothing to commit."
|
|
180
275
|
}
|
|
276
|
+
|
|
277
|
+
Log-Info "Pushing to origin/$Branch..."
|
|
278
|
+
git push origin $Branch
|
|
279
|
+
Log-Pass "reason_code=SYNC_PUSHED"
|
|
280
|
+
Pop-Location
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
2
|
# -------------------------------------------------------------------
|
|
3
3
|
# validate-and-push.sh — local test-fix-push loop
|
|
4
4
|
#
|
|
5
5
|
# Part of the its-magic quality chain:
|
|
6
6
|
# Cursor AI loop → validate-and-push → CI auto-fix (GitHub)
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# Bash is required for safe eval + optional timeout wrapping (DEC-0058 / parity
|
|
9
|
+
# with validate-and-push.ps1).
|
|
10
|
+
#
|
|
11
|
+
# Reads merged scratchpad (installer merge) for sync policy and runbook keys
|
|
12
|
+
# for commands. Opt-in push only when policy allows. See runbook (DEC-0058).
|
|
12
13
|
# -------------------------------------------------------------------
|
|
13
|
-
set -
|
|
14
|
+
set -euo pipefail
|
|
14
15
|
|
|
15
16
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
17
|
+
DRY_RUN=0
|
|
18
|
+
if [ "${1:-}" = "--dry-run" ]; then
|
|
19
|
+
DRY_RUN=1
|
|
20
|
+
shift
|
|
21
|
+
fi
|
|
22
|
+
|
|
16
23
|
MAX_ATTEMPTS="${1:-5}"
|
|
17
24
|
BRANCH="${2:-}"
|
|
18
25
|
AUTO_COMMIT="${3:-true}"
|
|
@@ -28,13 +35,54 @@ log_pass() { printf "${pass_color}[pass]${reset} %s\n" "$*"; }
|
|
|
28
35
|
log_fail() { printf "${fail_color}[fail]${reset} %s\n" "$*"; }
|
|
29
36
|
log_warn() { printf "${warn_color}[warn]${reset} %s\n" "$*"; }
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
resolve_python() {
|
|
39
|
+
if command -v python >/dev/null 2>&1; then printf "%s" "python"; return; fi
|
|
40
|
+
if command -v python3 >/dev/null 2>&1; then printf "%s" "python3"; return; fi
|
|
41
|
+
printf "%s" ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
run_with_timeout() {
|
|
45
|
+
local sec="$1"
|
|
46
|
+
shift
|
|
47
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
48
|
+
timeout "$sec" "$@"
|
|
49
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
50
|
+
gtimeout "$sec" "$@"
|
|
51
|
+
else
|
|
52
|
+
"$@"
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Run runbook command string from repo root; optional wall-clock cap.
|
|
57
|
+
run_runbook_cmd() {
|
|
58
|
+
local cmd="$1"
|
|
59
|
+
local kind="$2"
|
|
60
|
+
local sec="$3"
|
|
61
|
+
if command -v timeout >/dev/null 2>&1 || command -v gtimeout >/dev/null 2>&1; then
|
|
62
|
+
if ! run_with_timeout "$sec" bash -c 'cd "$1" && eval "$2"' _ "$ROOT" "$cmd"; then
|
|
63
|
+
local ec=$?
|
|
64
|
+
if [ "$kind" = "test" ]; then
|
|
65
|
+
if [ "$ec" = "124" ] || [ "$ec" = "143" ]; then log_fail "reason_code=TEST_TIMEOUT"; else log_fail "reason_code=TEST_FAILED"; fi
|
|
66
|
+
else
|
|
67
|
+
log_fail "reason_code=OPTIONAL_CHECK_FAILED"
|
|
68
|
+
fi
|
|
69
|
+
return 1
|
|
70
|
+
fi
|
|
71
|
+
else
|
|
72
|
+
if ! ( cd "$ROOT" && eval "$cmd" ); then
|
|
73
|
+
if [ "$kind" = "test" ]; then log_fail "reason_code=TEST_FAILED"; else log_fail "reason_code=OPTIONAL_CHECK_FAILED"; fi
|
|
74
|
+
return 1
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
32
79
|
|
|
33
80
|
RUNBOOK="$ROOT/docs/engineering/runbook.md"
|
|
34
81
|
|
|
35
82
|
read_runbook_key() {
|
|
36
|
-
key="$1"
|
|
83
|
+
local key="$1"
|
|
37
84
|
if [ ! -f "$RUNBOOK" ]; then return; fi
|
|
85
|
+
local value
|
|
38
86
|
value=$(sed -n "s/^${key}:[[:space:]]*\(.\{1,\}\)$/\1/p" "$RUNBOOK" | head -n 1)
|
|
39
87
|
case "$value" in
|
|
40
88
|
""|"..."|"<...>"|"TODO") return ;;
|
|
@@ -42,16 +90,40 @@ read_runbook_key() {
|
|
|
42
90
|
printf "%s" "$value"
|
|
43
91
|
}
|
|
44
92
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
93
|
+
run_sync_gate() {
|
|
94
|
+
local sub="$1"
|
|
95
|
+
local out err line gate_ok gate_rc
|
|
96
|
+
out="$(mktemp)"
|
|
97
|
+
err="$(mktemp)"
|
|
98
|
+
set +e
|
|
99
|
+
"$PY" "$GATE_SCRIPT" "$sub" --root "$ROOT" --branch "$BRANCH" >"$out" 2>"$err"
|
|
100
|
+
set -e
|
|
101
|
+
if [ -s "$err" ]; then cat "$err" >&2; fi
|
|
102
|
+
line="$(grep '^{' "$out" | tail -n 1)"
|
|
103
|
+
rm -f "$out" "$err"
|
|
104
|
+
if [ -z "$line" ]; then
|
|
105
|
+
log_fail "reason_code=SYNC_GATE_EMPTY_RESPONSE"
|
|
106
|
+
return 1
|
|
107
|
+
fi
|
|
108
|
+
gate_ok="$("$PY" -c "import json,sys; print(1 if json.loads(sys.argv[1]).get('ok') else 0)" "$line" 2>/dev/null || echo 0)"
|
|
109
|
+
gate_rc="$("$PY" -c "import json,sys; print(json.loads(sys.argv[1]).get('reason_code') or '')" "$line" 2>/dev/null || true)"
|
|
110
|
+
if [ "$gate_ok" != "1" ]; then
|
|
111
|
+
log_fail "reason_code=${gate_rc:-SYNC_GATE_BLOCK}"
|
|
112
|
+
return 1
|
|
113
|
+
fi
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
50
116
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
log_warn "
|
|
117
|
+
PY="$(resolve_python)"
|
|
118
|
+
if [ -z "$PY" ]; then
|
|
119
|
+
log_fail "reason_code=PYTHON_NOT_ON_PATH"
|
|
120
|
+
log_warn "Install Python 3 and ensure it is on PATH for merged scratchpad policy gates."
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
GATE_SCRIPT="$ROOT/scripts/sync_push_gates.py"
|
|
125
|
+
if [ ! -f "$GATE_SCRIPT" ]; then
|
|
126
|
+
log_fail "reason_code=SYNC_GATE_SCRIPT_MISSING"
|
|
55
127
|
exit 1
|
|
56
128
|
fi
|
|
57
129
|
|
|
@@ -61,11 +133,35 @@ fi
|
|
|
61
133
|
|
|
62
134
|
log_info "validate-and-push loop"
|
|
63
135
|
log_info "Branch: $BRANCH | Max attempts: $MAX_ATTEMPTS"
|
|
64
|
-
[
|
|
65
|
-
|
|
136
|
+
if [ "$DRY_RUN" = "1" ]; then log_info "Dry-run: no git push will run."; fi
|
|
137
|
+
printf "\n"
|
|
138
|
+
|
|
139
|
+
if ! run_sync_gate policy; then exit 1; fi
|
|
140
|
+
|
|
141
|
+
TEST_CMD=$(read_runbook_key "TEST_COMMAND")
|
|
142
|
+
LINT_CMD=$(read_runbook_key "LINT_COMMAND")
|
|
143
|
+
TYPECHECK_CMD=$(read_runbook_key "TYPECHECK_COMMAND")
|
|
144
|
+
LINT_FIX_CMD=$(read_runbook_key "LINT_FIX_COMMAND")
|
|
145
|
+
FORMAT_CMD=$(read_runbook_key "FORMAT_COMMAND")
|
|
146
|
+
TIMEOUT_RAW=$(read_runbook_key "TEST_TIMEOUT_SECONDS")
|
|
147
|
+
TEST_TIMEOUT_SEC=120
|
|
148
|
+
case "$TIMEOUT_RAW" in
|
|
149
|
+
''|*[!0-9]*) ;;
|
|
150
|
+
*) TEST_TIMEOUT_SEC="$TIMEOUT_RAW" ;;
|
|
151
|
+
esac
|
|
152
|
+
|
|
153
|
+
if [ -z "$TEST_CMD" ]; then
|
|
154
|
+
log_fail "reason_code=TEST_COMMAND_MISSING"
|
|
155
|
+
log_warn "TEST_COMMAND is required by sync policy for push-eligible paths."
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
[ -n "$TEST_CMD" ] && log_info "TEST_COMMAND: $TEST_CMD"
|
|
160
|
+
[ -n "$LINT_CMD" ] && log_info "LINT_COMMAND: $LINT_CMD"
|
|
66
161
|
[ -n "$TYPECHECK_CMD" ] && log_info "TYPECHECK_COMMAND: $TYPECHECK_CMD"
|
|
67
162
|
[ -n "$LINT_FIX_CMD" ] && log_info "LINT_FIX_COMMAND: $LINT_FIX_CMD"
|
|
68
|
-
[ -n "$FORMAT_CMD" ]
|
|
163
|
+
[ -n "$FORMAT_CMD" ] && log_info "FORMAT_COMMAND: $FORMAT_CMD"
|
|
164
|
+
log_info "TEST_TIMEOUT_SECONDS: $TEST_TIMEOUT_SEC"
|
|
69
165
|
printf "\n"
|
|
70
166
|
|
|
71
167
|
attempt=0
|
|
@@ -76,53 +172,29 @@ while [ "$attempt" -lt "$MAX_ATTEMPTS" ]; do
|
|
|
76
172
|
cd "$ROOT"
|
|
77
173
|
all_ok=true
|
|
78
174
|
|
|
79
|
-
# 1. Run formatter if available
|
|
80
175
|
if [ -n "$FORMAT_CMD" ]; then
|
|
81
176
|
log_info "Running formatter..."
|
|
82
|
-
if eval "$FORMAT_CMD"; then
|
|
83
|
-
log_pass "Format OK"
|
|
84
|
-
else
|
|
85
|
-
log_warn "Formatter reported issues (non-blocking)"
|
|
86
|
-
fi
|
|
177
|
+
if eval "$FORMAT_CMD"; then log_pass "Format OK"; else log_warn "Formatter reported issues (non-blocking)"; fi
|
|
87
178
|
fi
|
|
88
179
|
|
|
89
|
-
# 2. Try lint fix if available
|
|
90
180
|
if [ -n "$LINT_FIX_CMD" ]; then
|
|
91
181
|
log_info "Running lint auto-fix..."
|
|
92
182
|
eval "$LINT_FIX_CMD" || true
|
|
93
183
|
fi
|
|
94
184
|
|
|
95
|
-
# 3. Run lint check
|
|
96
185
|
if [ -n "$LINT_CMD" ]; then
|
|
97
186
|
log_info "Running lint check..."
|
|
98
|
-
if
|
|
99
|
-
log_pass "Lint OK"
|
|
100
|
-
else
|
|
101
|
-
log_fail "Lint failed"
|
|
102
|
-
all_ok=false
|
|
103
|
-
fi
|
|
187
|
+
if run_runbook_cmd "$LINT_CMD" "optional" "$TEST_TIMEOUT_SEC"; then log_pass "Lint OK"; else all_ok=false; fi
|
|
104
188
|
fi
|
|
105
189
|
|
|
106
|
-
# 4. Run typecheck (optional)
|
|
107
190
|
if [ -n "$TYPECHECK_CMD" ]; then
|
|
108
191
|
log_info "Running typecheck..."
|
|
109
|
-
if
|
|
110
|
-
log_pass "Typecheck OK"
|
|
111
|
-
else
|
|
112
|
-
log_fail "Typecheck failed"
|
|
113
|
-
all_ok=false
|
|
114
|
-
fi
|
|
192
|
+
if run_runbook_cmd "$TYPECHECK_CMD" "optional" "$TEST_TIMEOUT_SEC"; then log_pass "Typecheck OK"; else all_ok=false; fi
|
|
115
193
|
fi
|
|
116
194
|
|
|
117
|
-
# 5. Run tests
|
|
118
195
|
if [ -n "$TEST_CMD" ]; then
|
|
119
196
|
log_info "Running tests..."
|
|
120
|
-
if
|
|
121
|
-
log_pass "Tests OK"
|
|
122
|
-
else
|
|
123
|
-
log_fail "Tests failed"
|
|
124
|
-
all_ok=false
|
|
125
|
-
fi
|
|
197
|
+
if run_runbook_cmd "$TEST_CMD" "test" "$TEST_TIMEOUT_SEC"; then log_pass "Tests OK"; else all_ok=false; fi
|
|
126
198
|
fi
|
|
127
199
|
|
|
128
200
|
if [ "$all_ok" = "true" ]; then
|
|
@@ -134,7 +206,7 @@ while [ "$attempt" -lt "$MAX_ATTEMPTS" ]; do
|
|
|
134
206
|
log_fail "Reached max attempts ($MAX_ATTEMPTS). Aborting push."
|
|
135
207
|
printf "\n"
|
|
136
208
|
log_warn "Fix the issues above, then re-run:"
|
|
137
|
-
log_warn "
|
|
209
|
+
log_warn " bash scripts/validate-and-push.sh"
|
|
138
210
|
exit 1
|
|
139
211
|
fi
|
|
140
212
|
|
|
@@ -143,21 +215,29 @@ while [ "$attempt" -lt "$MAX_ATTEMPTS" ]; do
|
|
|
143
215
|
read -r _unused
|
|
144
216
|
done
|
|
145
217
|
|
|
218
|
+
if ! run_sync_gate post; then exit 1; fi
|
|
219
|
+
|
|
146
220
|
printf "\n"
|
|
147
221
|
|
|
148
|
-
if [ "$
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
log_info "Staging and committing changes..."
|
|
152
|
-
git add -A
|
|
153
|
-
git commit -m "fix: address check failures (validate-and-push)"
|
|
222
|
+
if [ "$DRY_RUN" = "1" ] || [ "$AUTO_COMMIT" != "true" ]; then
|
|
223
|
+
if [ "$DRY_RUN" = "1" ]; then
|
|
224
|
+
log_pass "reason_code=SYNC_PUSHED (dry-run; no git push)"
|
|
154
225
|
else
|
|
155
|
-
log_info "
|
|
226
|
+
log_info "Auto-commit disabled. Push manually when ready."
|
|
227
|
+
log_pass "reason_code=SYNC_PUSHED (checks only; push skipped by flag)"
|
|
156
228
|
fi
|
|
229
|
+
exit 0
|
|
230
|
+
fi
|
|
157
231
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
232
|
+
cd "$ROOT"
|
|
233
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
234
|
+
log_info "Staging and committing changes..."
|
|
235
|
+
git add -A
|
|
236
|
+
git commit -m "fix: address check failures (validate-and-push)"
|
|
161
237
|
else
|
|
162
|
-
log_info "
|
|
238
|
+
log_info "Working tree clean, nothing to commit."
|
|
163
239
|
fi
|
|
240
|
+
|
|
241
|
+
log_info "Pushing to origin/$BRANCH..."
|
|
242
|
+
git push origin "$BRANCH"
|
|
243
|
+
log_pass "reason_code=SYNC_PUSHED"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validate documentation profile surfaces (DEC-0059).
|
|
4
|
+
|
|
5
|
+
Reads merged scratchpad via installer.merge_scratchpad_layers (DEC-0058 pattern).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
# Repo root (parent of scripts/)
|
|
15
|
+
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
_REPO_ROOT = os.path.normpath(os.path.join(_SCRIPT_DIR, ".."))
|
|
17
|
+
|
|
18
|
+
if _REPO_ROOT not in sys.path:
|
|
19
|
+
sys.path.insert(0, _REPO_ROOT)
|
|
20
|
+
if _SCRIPT_DIR not in sys.path:
|
|
21
|
+
sys.path.insert(0, _SCRIPT_DIR)
|
|
22
|
+
|
|
23
|
+
import installer # noqa: E402
|
|
24
|
+
import doc_profile_lib # noqa: E402
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _fail(messages: list[str]) -> int:
|
|
28
|
+
for m in messages:
|
|
29
|
+
print(m, file=sys.stderr)
|
|
30
|
+
return 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> int:
|
|
34
|
+
parser = argparse.ArgumentParser(description="Validate README/doc profile vs merged scratchpad.")
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--repo",
|
|
37
|
+
default=_REPO_ROOT,
|
|
38
|
+
help="Target repository root (default: parent of scripts/).",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--no-template-parity",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="Skip active vs template/ parity (for fixture-only trees).",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--self-test",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Run resolver matrix self-test and exit.",
|
|
49
|
+
)
|
|
50
|
+
args = parser.parse_args()
|
|
51
|
+
|
|
52
|
+
if args.self_test:
|
|
53
|
+
doc_profile_lib.self_test_resolver()
|
|
54
|
+
print("[DOC_PROFILE_SELF_TEST_OK]")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
target = os.path.abspath(args.repo)
|
|
58
|
+
template_root = None if args.no_template_parity else os.path.join(target, "template")
|
|
59
|
+
|
|
60
|
+
merged, paths = installer.merge_scratchpad_layers(target)
|
|
61
|
+
merge_errors: list[str] = []
|
|
62
|
+
if not os.path.isfile(paths["example"]):
|
|
63
|
+
merge_errors.append(
|
|
64
|
+
"[DOC_PROFILE_MERGE_ERROR] EXAMPLE_LAYER_MISSING: "
|
|
65
|
+
f".cursor/scratchpad.local.example.md not found under {target}."
|
|
66
|
+
)
|
|
67
|
+
if not os.path.isfile(paths["baseline"]):
|
|
68
|
+
merge_errors.append(
|
|
69
|
+
"[DOC_PROFILE_MERGE_ERROR] MATERIALIZED_BASELINE_MISSING: "
|
|
70
|
+
f".cursor/scratchpad.md not found under {target}."
|
|
71
|
+
)
|
|
72
|
+
if merge_errors:
|
|
73
|
+
return _fail(merge_errors)
|
|
74
|
+
|
|
75
|
+
ok, scratch_diagnostics = installer.validate_merged_scratchpad(target)
|
|
76
|
+
if not ok:
|
|
77
|
+
out = [ln for ln in scratch_diagnostics if "SCRATCHPAD_MERGE_ERROR" in ln or "REQUIRED_KEY" in ln]
|
|
78
|
+
mapped = [
|
|
79
|
+
ln.replace("[SCRATCHPAD_MERGE_ERROR]", "[DOC_PROFILE_MERGE_ERROR]", 1)
|
|
80
|
+
if ln.startswith("[SCRATCHPAD_MERGE_ERROR]")
|
|
81
|
+
else f"[DOC_PROFILE_MERGE_ERROR] {ln}"
|
|
82
|
+
for ln in (out or scratch_diagnostics)
|
|
83
|
+
]
|
|
84
|
+
return _fail(mapped)
|
|
85
|
+
|
|
86
|
+
readme_path = os.path.join(target, "README.md")
|
|
87
|
+
readme_txt = ""
|
|
88
|
+
if os.path.isfile(readme_path):
|
|
89
|
+
with open(readme_path, "r", encoding="utf-8") as f:
|
|
90
|
+
readme_txt = f.read()
|
|
91
|
+
for w in doc_profile_lib.optional_mode_warnings(merged, readme_txt):
|
|
92
|
+
print(w, file=sys.stderr)
|
|
93
|
+
|
|
94
|
+
errs = doc_profile_lib.validate_repo_doc_profile(target, merged, template_root)
|
|
95
|
+
if errs:
|
|
96
|
+
return _fail(errs)
|
|
97
|
+
|
|
98
|
+
print("[DOC_PROFILE_VALIDATE_OK]")
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
raise SystemExit(main())
|