create-caspian-app 1.5.8 → 1.6.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.
- package/dist/.github/copilot-instructions.md +261 -260
- package/dist/AGENTS.md +445 -434
- package/dist/index.js +2 -2
- package/dist/postcss.config.js +11 -1
- package/dist/pyproject.toml +47 -47
- package/dist/settings/browser_log.py +536 -505
- package/dist/settings/bs-config.json +6 -6
- package/dist/settings/bs-config.ts +32 -0
- package/dist/settings/check.py +418 -418
- package/dist/settings/check_templates.py +497 -497
- package/dist/settings/dev-hold.ts +223 -0
- package/dist/settings/fix.py +113 -113
- package/dist/settings/format.py +719 -719
- package/dist/settings/run-postcss.ts +13 -9
- package/dist/settings/utils.ts +28 -0
- package/dist/src/app/globals.css +9 -0
- package/dist/src/app/globals.plain.css +502 -0
- package/dist/tests/README.md +250 -250
- package/dist/tests/test_health_route.py +85 -42
- package/package.json +1 -1
- package/dist/public/css/index.css +0 -1
- package/dist/public/css/styles.css +0 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-stack reload hold.
|
|
3
|
+
*
|
|
4
|
+
* The file watcher cannot tell an agent's write from a human's save — chokidar
|
|
5
|
+
* sees an inode change, not a writer. So the agent declares itself instead: a
|
|
6
|
+
* hold file marks "an agent is mid-run", and while it is active the change
|
|
7
|
+
* coordinator in `bs-config.ts` keeps queueing changes without restarting the
|
|
8
|
+
* Python server or reloading the browser. Releasing the hold drains the whole
|
|
9
|
+
* run as ONE restart and ONE reload.
|
|
10
|
+
*
|
|
11
|
+
* Why this matters: the coordinator batches on a 1500 ms quiet period, which is
|
|
12
|
+
* tuned for a human's burst-save. An agent's gap between two edits is a tool
|
|
13
|
+
* round-trip — always wider than that window — so without a hold every single
|
|
14
|
+
* edit costs a full process restart plus a reload of every open tab, and each
|
|
15
|
+
* reload re-runs the route's Prisma queries against a pool that was just
|
|
16
|
+
* discarded. Against a remote database that is the churn this file prevents.
|
|
17
|
+
*
|
|
18
|
+
* The signal is set by Claude Code hooks (`PreToolUse` acquires and refreshes,
|
|
19
|
+
* `Stop`/`SessionEnd` release), so it does not depend on a model remembering to
|
|
20
|
+
* call anything. The manual escape hatches are `npm run dev:hold`,
|
|
21
|
+
* `npm run dev:resume`, and `npm run dev:hold:status`.
|
|
22
|
+
*
|
|
23
|
+
* This module is deliberately dependency-free and written in erasable-only
|
|
24
|
+
* TypeScript so Node can run it directly (`node settings/dev-hold.ts acquire`)
|
|
25
|
+
* with a cold start fast enough to sit in front of every agent edit.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A hold that stops being refreshed is treated as abandoned. This is the valve
|
|
34
|
+
* that keeps a crashed agent or a killed session from freezing the dev stack:
|
|
35
|
+
* the worst case degrades to today's behaviour, never to a dead server.
|
|
36
|
+
*/
|
|
37
|
+
export const STALE_HOLD_MS = 120_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Even a continuously refreshed hold drains eventually, so a very long agent run
|
|
41
|
+
* still gets periodic restarts instead of none at all.
|
|
42
|
+
*/
|
|
43
|
+
export const MAX_HOLD_MS = 600_000;
|
|
44
|
+
|
|
45
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
|
|
47
|
+
/** Resolved from this file, not from `cwd`, so hook shells cannot misplace it. */
|
|
48
|
+
export const WORKSPACE_ROOT = join(MODULE_DIR, "..");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lives in `.casp/` on purpose: `npm run dev` deletes that directory at startup,
|
|
52
|
+
* so a fresh dev stack can never inherit a stale hold from a previous session.
|
|
53
|
+
*
|
|
54
|
+
* Resolved per call rather than at import so `CASPIAN_DEV_HOLD_PATH` can point a
|
|
55
|
+
* test (or a second stack) at its own file instead of the live one.
|
|
56
|
+
*/
|
|
57
|
+
export function getHoldPath(): string {
|
|
58
|
+
return (
|
|
59
|
+
process.env.CASPIAN_DEV_HOLD_PATH ||
|
|
60
|
+
join(WORKSPACE_ROOT, ".casp", "dev-hold.json")
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type DevHold = {
|
|
65
|
+
/** Who asked for the hold, for the terminal message only. */
|
|
66
|
+
owner: string;
|
|
67
|
+
/** Process that acquired it, for debugging a hold nobody expected. */
|
|
68
|
+
pid: number;
|
|
69
|
+
/** First acquire — drives the absolute cap. */
|
|
70
|
+
acquiredAt: number;
|
|
71
|
+
/** Most recent acquire — drives the stale check. */
|
|
72
|
+
touchedAt: number;
|
|
73
|
+
/** How many edits this run has queued, so the terminal can show progress. */
|
|
74
|
+
edits: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type DevHoldStatus =
|
|
78
|
+
| { active: false; reason: "none"; hold: null }
|
|
79
|
+
| { active: false; reason: "stale" | "expired"; hold: DevHold }
|
|
80
|
+
| { active: true; reason: "held"; hold: DevHold };
|
|
81
|
+
|
|
82
|
+
function isDevHold(value: unknown): value is DevHold {
|
|
83
|
+
if (typeof value !== "object" || value === null) return false;
|
|
84
|
+
const candidate = value as Partial<DevHold>;
|
|
85
|
+
return (
|
|
86
|
+
typeof candidate.owner === "string" &&
|
|
87
|
+
typeof candidate.pid === "number" &&
|
|
88
|
+
typeof candidate.acquiredAt === "number" &&
|
|
89
|
+
typeof candidate.touchedAt === "number" &&
|
|
90
|
+
typeof candidate.edits === "number"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Returns the parsed hold, or null when absent or unreadable. */
|
|
95
|
+
export function readDevHold(): DevHold | null {
|
|
96
|
+
try {
|
|
97
|
+
const parsed: unknown = JSON.parse(readFileSync(getHoldPath(), "utf-8"));
|
|
98
|
+
return isDevHold(parsed) ? parsed : null;
|
|
99
|
+
} catch {
|
|
100
|
+
// Missing, half-written, or corrupt all mean the same thing to a caller:
|
|
101
|
+
// there is no hold it can trust, so fall through to normal reloading.
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Decide whether the coordinator should defer. Both expiry rules fail open —
|
|
108
|
+
* an unreadable, stale, or over-long hold reports inactive rather than blocking
|
|
109
|
+
* the dev stack forever.
|
|
110
|
+
*/
|
|
111
|
+
export function evaluateDevHold(now: number = Date.now()): DevHoldStatus {
|
|
112
|
+
const hold = readDevHold();
|
|
113
|
+
if (!hold) return { active: false, reason: "none", hold: null };
|
|
114
|
+
|
|
115
|
+
if (now - hold.touchedAt > STALE_HOLD_MS) {
|
|
116
|
+
return { active: false, reason: "stale", hold };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (now - hold.acquiredAt > MAX_HOLD_MS) {
|
|
120
|
+
return { active: false, reason: "expired", hold };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { active: true, reason: "held", hold };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** True when reloads should be deferred right now. */
|
|
127
|
+
export function isDevHoldActive(now: number = Date.now()): boolean {
|
|
128
|
+
return evaluateDevHold(now).active;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create the hold, or refresh an existing one. Refreshing keeps `acquiredAt` so
|
|
133
|
+
* the absolute cap measures the whole run rather than restarting on every edit.
|
|
134
|
+
*
|
|
135
|
+
* A capped-out hold is deliberately still carried over. If a new edit started a
|
|
136
|
+
* fresh window instead, an agent editing steadily would re-hold before the
|
|
137
|
+
* coordinator's next 1500 ms tick and the cap would reset forever without ever
|
|
138
|
+
* forcing a drain. Carrying it keeps the run expired — so reloads resume — until
|
|
139
|
+
* an explicit release ends the run. A stale hold is a different case: nothing
|
|
140
|
+
* refreshed it, so its owner is gone and the next edit legitimately starts over.
|
|
141
|
+
*/
|
|
142
|
+
export function acquireDevHold(
|
|
143
|
+
owner: string = "agent",
|
|
144
|
+
now: number = Date.now(),
|
|
145
|
+
): DevHold {
|
|
146
|
+
const previous = evaluateDevHold(now);
|
|
147
|
+
const carryOver =
|
|
148
|
+
previous.reason === "held" || previous.reason === "expired"
|
|
149
|
+
? previous.hold
|
|
150
|
+
: null;
|
|
151
|
+
|
|
152
|
+
const hold: DevHold = {
|
|
153
|
+
owner,
|
|
154
|
+
pid: process.pid,
|
|
155
|
+
acquiredAt: carryOver ? carryOver.acquiredAt : now,
|
|
156
|
+
touchedAt: now,
|
|
157
|
+
edits: carryOver ? carryOver.edits + 1 : 1,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
mkdirSync(dirname(getHoldPath()), { recursive: true });
|
|
161
|
+
writeFileSync(getHoldPath(), `${JSON.stringify(hold, null, 2)}\n`, "utf-8");
|
|
162
|
+
return hold;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Drop the hold. Idempotent, so a duplicate `Stop` hook is harmless. */
|
|
166
|
+
export function releaseDevHold(): DevHold | null {
|
|
167
|
+
const hold = readDevHold();
|
|
168
|
+
rmSync(getHoldPath(), { force: true });
|
|
169
|
+
return hold;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function describeStatus(status: DevHoldStatus): string {
|
|
173
|
+
if (status.reason === "none") return "inactive - no hold file";
|
|
174
|
+
|
|
175
|
+
const ageSeconds = Math.round((Date.now() - status.hold.acquiredAt) / 1000);
|
|
176
|
+
const summary = `owner=${status.hold.owner} pid=${status.hold.pid} edits=${status.hold.edits} age=${ageSeconds}s`;
|
|
177
|
+
|
|
178
|
+
if (status.reason === "stale") {
|
|
179
|
+
return `inactive - hold went stale (no refresh for over ${STALE_HOLD_MS / 1000}s); ${summary}`;
|
|
180
|
+
}
|
|
181
|
+
if (status.reason === "expired") {
|
|
182
|
+
return `inactive - hold passed the ${MAX_HOLD_MS / 1000}s cap; ${summary}`;
|
|
183
|
+
}
|
|
184
|
+
return `ACTIVE - browser reloads and Python restarts are deferred; ${summary}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function runCli(argv: string[]): number {
|
|
188
|
+
const command = argv[0] ?? "status";
|
|
189
|
+
const quiet = argv.includes("--quiet");
|
|
190
|
+
const log = (message: string) => {
|
|
191
|
+
if (!quiet) console.log(message);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (command === "acquire") {
|
|
195
|
+
const hold = acquireDevHold(process.env.CASPIAN_DEV_HOLD_OWNER || "agent");
|
|
196
|
+
log(`[dev-hold] Held after ${hold.edits} edit(s); reloads deferred.`);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (command === "release") {
|
|
201
|
+
const hold = releaseDevHold();
|
|
202
|
+
log(
|
|
203
|
+
hold
|
|
204
|
+
? `[dev-hold] Released after ${hold.edits} edit(s); the dev stack will reload once.`
|
|
205
|
+
: "[dev-hold] No hold was active.",
|
|
206
|
+
);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (command === "status") {
|
|
211
|
+
console.log(`[dev-hold] ${describeStatus(evaluateDevHold())}`);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.error(`[dev-hold] Unknown command: ${command}`);
|
|
216
|
+
console.error("[dev-hold] Usage: dev-hold.ts <acquire|release|status> [--quiet]");
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Only act as a CLI when executed directly; `bs-config.ts` imports the helpers.
|
|
221
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
222
|
+
process.exit(runCli(process.argv.slice(2)));
|
|
223
|
+
}
|
package/dist/settings/fix.py
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
"""Safe auto-fixer for the app (`npm run
|
|
2
|
-
|
|
3
|
-
Removes genuinely dead imports and applies ruff's other safe fixes, then runs
|
|
4
|
-
the gate to report what remains. The catch it exists to handle: `pyproject.toml`
|
|
5
|
-
marks `F401` unfixable so a plain `ruff check --fix` (which anyone might run)
|
|
6
|
-
can never silently delete a component import that is used only as an `<x-*>` tag
|
|
7
|
-
(casp resolves those from module globals at render time). That safety also
|
|
8
|
-
blocks auto-removal of *ordinary* dead imports, so this script re-enables F401
|
|
9
|
-
removal — but only for files that contain no component-tag import, so component
|
|
10
|
-
imports are never at risk.
|
|
11
|
-
|
|
12
|
-
Flow:
|
|
13
|
-
1. Format first (`format.py`): `ruff format` for Python, djLint for the markup
|
|
14
|
-
inside `html(r\"\"\"...\"\"\")`. Formatting settles layout before anything
|
|
15
|
-
inspects the code, so the fixer and the gate report on the final shape
|
|
16
|
-
rather than on lines the formatter is about to move.
|
|
17
|
-
2. List F401 findings under the project config (respects include/exclude).
|
|
18
|
-
3. Split the owning files into "component-guarded" (has >=1 import used as an
|
|
19
|
-
`<x-*>` tag) and the rest.
|
|
20
|
-
4. Remove dead imports only in the non-guarded files, via an isolated ruff run
|
|
21
|
-
(`--isolated` makes F401 fixable again; `--select F401` limits it to import
|
|
22
|
-
removal; explicit file args keep the run scoped).
|
|
23
|
-
5. Apply every other safe fix under the real project config.
|
|
24
|
-
6. Run the gate (`check.py`) and exit with its status.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
from __future__ import annotations
|
|
28
|
-
|
|
29
|
-
import json
|
|
30
|
-
import subprocess
|
|
31
|
-
import sys
|
|
32
|
-
from pathlib import Path
|
|
33
|
-
|
|
34
|
-
import _component_imports as ci
|
|
35
|
-
|
|
36
|
-
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _run(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|
40
|
-
return subprocess.run(
|
|
41
|
-
cmd,
|
|
42
|
-
cwd=PROJECT_ROOT,
|
|
43
|
-
capture_output=True,
|
|
44
|
-
text=True,
|
|
45
|
-
encoding="utf-8",
|
|
46
|
-
errors="replace",
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _ruff(*args: str) -> subprocess.CompletedProcess[str]:
|
|
51
|
-
return _run([sys.executable, "-m", "ruff", "check", *args])
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _remove_dead_imports() -> list[str]:
|
|
55
|
-
"""Delete genuinely unused imports; never touch component-tag imports.
|
|
56
|
-
|
|
57
|
-
Returns the list of files that were auto-fixed.
|
|
58
|
-
"""
|
|
59
|
-
proc = _ruff(".", "--select", "F401", "--output-format", "json")
|
|
60
|
-
try:
|
|
61
|
-
findings = json.loads(proc.stdout or "[]")
|
|
62
|
-
except json.JSONDecodeError:
|
|
63
|
-
return []
|
|
64
|
-
|
|
65
|
-
guarded: set[str] = set()
|
|
66
|
-
owning_files: set[str] = set()
|
|
67
|
-
for f in findings:
|
|
68
|
-
path = f.get("filename", "")
|
|
69
|
-
if not path:
|
|
70
|
-
continue
|
|
71
|
-
owning_files.add(path)
|
|
72
|
-
if ci.is_component_tag_f401(f.get("message", ""), path):
|
|
73
|
-
guarded.add(path)
|
|
74
|
-
|
|
75
|
-
# A file with even one template-driven import is skipped whole: we can't
|
|
76
|
-
# remove just its dead imports without risking the load-bearing ones, and
|
|
77
|
-
# the gate still reports any real deadwood there for manual removal.
|
|
78
|
-
fixable_files = sorted(owning_files - guarded)
|
|
79
|
-
if fixable_files:
|
|
80
|
-
_ruff(*fixable_files, "--select", "F401", "--fix-only", "--isolated")
|
|
81
|
-
return fixable_files
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _format() -> None:
|
|
85
|
-
"""Run the formatter, but never let it block the fixer or the gate.
|
|
86
|
-
|
|
87
|
-
A formatting problem (a missing djLint, an unformattable block) must not
|
|
88
|
-
stop dead imports from being removed or the gate from reporting. `format.py`
|
|
89
|
-
prints its own summary, including anything it skipped.
|
|
90
|
-
"""
|
|
91
|
-
subprocess.run(
|
|
92
|
-
[sys.executable, str(PROJECT_ROOT / "settings" / "format.py")],
|
|
93
|
-
cwd=PROJECT_ROOT,
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def main() -> int:
|
|
98
|
-
_format()
|
|
99
|
-
_remove_dead_imports()
|
|
100
|
-
# Every other safe fix under the real project config. F401 stays unfixable
|
|
101
|
-
# here (dead imports were already handled above), so component imports are
|
|
102
|
-
# untouched.
|
|
103
|
-
_ruff(".", "--fix-only")
|
|
104
|
-
|
|
105
|
-
gate = subprocess.run(
|
|
106
|
-
[sys.executable, str(PROJECT_ROOT / "settings" / "check.py")],
|
|
107
|
-
cwd=PROJECT_ROOT,
|
|
108
|
-
)
|
|
109
|
-
return gate.returncode
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if __name__ == "__main__":
|
|
113
|
-
raise SystemExit(main())
|
|
1
|
+
"""Safe auto-fixer for the app (`npm run test:fix`).
|
|
2
|
+
|
|
3
|
+
Removes genuinely dead imports and applies ruff's other safe fixes, then runs
|
|
4
|
+
the gate to report what remains. The catch it exists to handle: `pyproject.toml`
|
|
5
|
+
marks `F401` unfixable so a plain `ruff check --fix` (which anyone might run)
|
|
6
|
+
can never silently delete a component import that is used only as an `<x-*>` tag
|
|
7
|
+
(casp resolves those from module globals at render time). That safety also
|
|
8
|
+
blocks auto-removal of *ordinary* dead imports, so this script re-enables F401
|
|
9
|
+
removal — but only for files that contain no component-tag import, so component
|
|
10
|
+
imports are never at risk.
|
|
11
|
+
|
|
12
|
+
Flow:
|
|
13
|
+
1. Format first (`format.py`): `ruff format` for Python, djLint for the markup
|
|
14
|
+
inside `html(r\"\"\"...\"\"\")`. Formatting settles layout before anything
|
|
15
|
+
inspects the code, so the fixer and the gate report on the final shape
|
|
16
|
+
rather than on lines the formatter is about to move.
|
|
17
|
+
2. List F401 findings under the project config (respects include/exclude).
|
|
18
|
+
3. Split the owning files into "component-guarded" (has >=1 import used as an
|
|
19
|
+
`<x-*>` tag) and the rest.
|
|
20
|
+
4. Remove dead imports only in the non-guarded files, via an isolated ruff run
|
|
21
|
+
(`--isolated` makes F401 fixable again; `--select F401` limits it to import
|
|
22
|
+
removal; explicit file args keep the run scoped).
|
|
23
|
+
5. Apply every other safe fix under the real project config.
|
|
24
|
+
6. Run the gate (`check.py`) and exit with its status.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
import _component_imports as ci
|
|
35
|
+
|
|
36
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|
40
|
+
return subprocess.run(
|
|
41
|
+
cmd,
|
|
42
|
+
cwd=PROJECT_ROOT,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
encoding="utf-8",
|
|
46
|
+
errors="replace",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ruff(*args: str) -> subprocess.CompletedProcess[str]:
|
|
51
|
+
return _run([sys.executable, "-m", "ruff", "check", *args])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _remove_dead_imports() -> list[str]:
|
|
55
|
+
"""Delete genuinely unused imports; never touch component-tag imports.
|
|
56
|
+
|
|
57
|
+
Returns the list of files that were auto-fixed.
|
|
58
|
+
"""
|
|
59
|
+
proc = _ruff(".", "--select", "F401", "--output-format", "json")
|
|
60
|
+
try:
|
|
61
|
+
findings = json.loads(proc.stdout or "[]")
|
|
62
|
+
except json.JSONDecodeError:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
guarded: set[str] = set()
|
|
66
|
+
owning_files: set[str] = set()
|
|
67
|
+
for f in findings:
|
|
68
|
+
path = f.get("filename", "")
|
|
69
|
+
if not path:
|
|
70
|
+
continue
|
|
71
|
+
owning_files.add(path)
|
|
72
|
+
if ci.is_component_tag_f401(f.get("message", ""), path):
|
|
73
|
+
guarded.add(path)
|
|
74
|
+
|
|
75
|
+
# A file with even one template-driven import is skipped whole: we can't
|
|
76
|
+
# remove just its dead imports without risking the load-bearing ones, and
|
|
77
|
+
# the gate still reports any real deadwood there for manual removal.
|
|
78
|
+
fixable_files = sorted(owning_files - guarded)
|
|
79
|
+
if fixable_files:
|
|
80
|
+
_ruff(*fixable_files, "--select", "F401", "--fix-only", "--isolated")
|
|
81
|
+
return fixable_files
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _format() -> None:
|
|
85
|
+
"""Run the formatter, but never let it block the fixer or the gate.
|
|
86
|
+
|
|
87
|
+
A formatting problem (a missing djLint, an unformattable block) must not
|
|
88
|
+
stop dead imports from being removed or the gate from reporting. `format.py`
|
|
89
|
+
prints its own summary, including anything it skipped.
|
|
90
|
+
"""
|
|
91
|
+
subprocess.run(
|
|
92
|
+
[sys.executable, str(PROJECT_ROOT / "settings" / "format.py")],
|
|
93
|
+
cwd=PROJECT_ROOT,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main() -> int:
|
|
98
|
+
_format()
|
|
99
|
+
_remove_dead_imports()
|
|
100
|
+
# Every other safe fix under the real project config. F401 stays unfixable
|
|
101
|
+
# here (dead imports were already handled above), so component imports are
|
|
102
|
+
# untouched.
|
|
103
|
+
_ruff(".", "--fix-only")
|
|
104
|
+
|
|
105
|
+
gate = subprocess.run(
|
|
106
|
+
[sys.executable, str(PROJECT_ROOT / "settings" / "check.py")],
|
|
107
|
+
cwd=PROJECT_ROOT,
|
|
108
|
+
)
|
|
109
|
+
return gate.returncode
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
raise SystemExit(main())
|