code-foundry 1.43.2 → 1.44.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/AGENTS.md +8 -0
- package/CHANGELOG.md +7 -0
- package/README.md +3 -2
- package/docs/CONFIGURATION.md +31 -0
- package/package.json +1 -1
- package/src/lib/pre-commit.mjs +114 -0
- package/src/runtime-core.mjs +74 -14
package/AGENTS.md
CHANGED
|
@@ -211,6 +211,14 @@ At minimum:
|
|
|
211
211
|
|
|
212
212
|
If a check cannot run, state the exact reason. A skipped check is not a passing check.
|
|
213
213
|
|
|
214
|
+
The generated `.githooks/pre-commit` gate is change-aware so parallel work on a
|
|
215
|
+
shared checkout is not serialized behind repository-wide checks. It blocks
|
|
216
|
+
whitespace errors, formats and lints only staged files, runs the project's
|
|
217
|
+
type-check command only when typed sources or compiler configuration are
|
|
218
|
+
staged, and does not build unless `pre_commit_build: true` is set. Passing the
|
|
219
|
+
hook is not full validation: run the broader checks above before opening or
|
|
220
|
+
readying a pull request.
|
|
221
|
+
|
|
214
222
|
<!-- /code-foundry-managed: validation -->
|
|
215
223
|
|
|
216
224
|
<!-- code-foundry-managed: tests-and-coverage -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.44.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.2...v1.44.0) (2026-10-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **pre-commit:** make the commit gate change-aware ([#702](https://github.com/0xPlayerOne/code-foundry/issues/702)) ([f5bca91](https://github.com/0xPlayerOne/code-foundry/commit/f5bca91c177b30b99f5d62287255d191617b4788))
|
|
9
|
+
|
|
3
10
|
## [1.43.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.1...v1.43.2) (2026-10-03)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -44,8 +44,9 @@ contract. For normal updates, edit that file and run `npx code-foundry sync`.
|
|
|
44
44
|
outcomes comparable across revisions and executors.
|
|
45
45
|
- An opt-in product-quality runner for static sites, web apps, Workers, and
|
|
46
46
|
published packages.
|
|
47
|
-
- A small `.githooks/pre-commit` launcher with
|
|
48
|
-
|
|
47
|
+
- A small `.githooks/pre-commit` launcher with a change-aware gate: it formats
|
|
48
|
+
and lints only staged files, type-checks only when typed sources are staged,
|
|
49
|
+
and leaves the build to CI.
|
|
49
50
|
- An optional `.mise.toml`/`mise.lock`, repository profile configuration, and
|
|
50
51
|
standard GitHub forms and policy files.
|
|
51
52
|
- AGENTS instructions, CODEOWNERS, license/notice files, and reusable release
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -131,6 +131,7 @@ See [Merge queue validation](merge-queues.md) before enabling it.
|
|
|
131
131
|
| `coverage_minimum` | `0`–`100` | Minimum percentage; defaults to `80`. |
|
|
132
132
|
| `coverage_metrics` | `lines`, `functions`, `branches`, `statements` | Metrics checked by the coverage gate. |
|
|
133
133
|
| `coverage_report` | comma-separated repository paths | Istanbul JSON summary or LCOV evidence files. |
|
|
134
|
+
| `pre_commit_build` | `true`, `false` | Run the project's build in the local pre-commit gate. Defaults to `false`; see [Pre-commit gate](#pre-commit-gate). |
|
|
134
135
|
| `filter_<task>` | comma-separated repository path globs | Skip a lane when no changed path matches; see [Lane path filters](#lane-path-filters). |
|
|
135
136
|
| `rust_sccache` | `true`, `false` | Install sccache and wrap rustc so compile artifacts survive manifest/lockfile churn. |
|
|
136
137
|
| `rust_nextest` | `true`, `false` | Install cargo-nextest and prefer `cargo nextest run` over `cargo test` in Rust test lanes. |
|
|
@@ -169,6 +170,36 @@ to keep discovery optional. Product-quality profiles are repository-owned
|
|
|
169
170
|
manifests invoked by existing build or E2E commands; they are not activated by a
|
|
170
171
|
configuration key. See [Product quality profiles](product-quality.md).
|
|
171
172
|
|
|
173
|
+
### Pre-commit gate
|
|
174
|
+
|
|
175
|
+
`code-foundry sync` installs `.githooks/pre-commit`, which runs
|
|
176
|
+
`code-foundry pre-commit` when the repository depends on `code-foundry`. The
|
|
177
|
+
gate is change-aware and never runs work for code the commit cannot affect:
|
|
178
|
+
|
|
179
|
+
- With nothing staged it exits immediately. `git diff --cached --check` still
|
|
180
|
+
blocks whitespace errors in every staged change.
|
|
181
|
+
- Oxfmt and Oxlint check only the staged files they support, honoring their
|
|
182
|
+
own ignore patterns. Staging `.oxfmtrc.json`, `.oxlintrc.json`, or their
|
|
183
|
+
`*.config.*` equivalents widens that tool to the whole repository through the
|
|
184
|
+
repository's `format:check`/`lint` script. A repository without Oxfmt or
|
|
185
|
+
Oxlint keeps its own script, which runs only when a matching file is staged.
|
|
186
|
+
- The project's `type-check`/`typecheck`/`type:check` script (or `tsc --noEmit`)
|
|
187
|
+
runs only when `.ts`, `.tsx`, `.mts`, `.cts`, or `tsconfig*.json` files are
|
|
188
|
+
staged, and for JavaScript sources when the repository type-checks them with
|
|
189
|
+
a root `jsconfig.json` or `checkJs`.
|
|
190
|
+
- Ruff checks only staged Python files (`--force-exclude`), or the whole
|
|
191
|
+
repository when `pyproject.toml`, `ruff.toml`, or a requirements file is
|
|
192
|
+
staged. Staged Rust sources or `Cargo.toml` run `cargo fmt --check` and
|
|
193
|
+
Clippy.
|
|
194
|
+
- The build does not run at commit time; CI and explicit commands own it. Set
|
|
195
|
+
`pre_commit_build: true` to run the `build` script (or `cargo build`) when
|
|
196
|
+
code is staged.
|
|
197
|
+
|
|
198
|
+
Tools read working-tree contents, so a partially staged file is checked as it
|
|
199
|
+
exists on disk. Task scripts that declare dependencies of their own, such as a
|
|
200
|
+
Turborepo `typecheck` task with `dependsOn: ["^build"]`, still run them; drop
|
|
201
|
+
that dependency to keep type checks build-free.
|
|
202
|
+
|
|
172
203
|
## Release and branch policy
|
|
173
204
|
|
|
174
205
|
| Key | Values | Purpose |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
|
|
5
5
|
"homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change-aware planning for the local commit gate.
|
|
5
|
+
*
|
|
6
|
+
* The gate runs once per commit and blocks every agent or developer sharing a
|
|
7
|
+
* checkout, so it only does work the staged change can affect: format and lint
|
|
8
|
+
* the staged files, type-check only when a typed source or compiler config is
|
|
9
|
+
* staged, and leave the build to CI unless a repository opts back in with
|
|
10
|
+
* `pre_commit_build: true`. Repository-wide runs happen only when a staged file
|
|
11
|
+
* changes the tool's own configuration, because that can affect every file.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Files Oxfmt formats; anything else is left out of the staged file list. */
|
|
15
|
+
const FORMATTABLE =
|
|
16
|
+
/\.(?:[cm]?[jt]sx?|json[c5]?|mdx?|ya?ml|toml|css|scss|less|html?|vue|svelte|astro|graphql|gql|hbs|handlebars)$/i
|
|
17
|
+
/** Files Oxlint lints. */
|
|
18
|
+
const LINTABLE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i
|
|
19
|
+
/** Typed sources and compiler configuration: the type-check trigger. */
|
|
20
|
+
const TYPED = /\.(?:ts|tsx|mts|cts)$|(?:^|\/)tsconfig[^/]*\.json$/i
|
|
21
|
+
/** JavaScript sources, type-checked only when the repository opts in with a root jsconfig. */
|
|
22
|
+
const JAVASCRIPT = /\.(?:js|jsx|mjs|cjs)$|(?:^|\/)jsconfig[^/]*\.json$/i
|
|
23
|
+
const FORMAT_CONFIG = /^(?:\.oxfmtrc\.jsonc?|oxfmt\.config\.[^/]+|\.prettierignore)$/
|
|
24
|
+
const LINT_CONFIG = /^(?:\.oxlintrc\.json|oxlint\.config\.[^/]+)$/
|
|
25
|
+
const PYTHON = /\.pyi?$/i
|
|
26
|
+
const PYTHON_CONFIG =
|
|
27
|
+
/^(?:pyproject\.toml|ruff\.toml|\.ruff\.toml)$|(?:^|\/)requirements[^/]*\.txt$/
|
|
28
|
+
const RUST = /\.rs$|(?:^|\/)Cargo\.toml$/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} PreCommitPlan
|
|
32
|
+
* @property {boolean} formatAll A formatter configuration file is staged.
|
|
33
|
+
* @property {string[]} formatFiles Staged files the formatter should check.
|
|
34
|
+
* @property {boolean} lintAll A linter configuration file is staged.
|
|
35
|
+
* @property {string[]} lintFiles Staged files the linter should check.
|
|
36
|
+
* @property {boolean} typeCheck Run the project's type-check command.
|
|
37
|
+
* @property {boolean} pythonAll A Python tool configuration file is staged.
|
|
38
|
+
* @property {string[]} pythonFiles Staged Python files.
|
|
39
|
+
* @property {boolean} rust Rust sources or manifests are staged.
|
|
40
|
+
* @property {boolean} build Run the project's build command.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string[]} staged Repository-relative paths of staged files that still exist.
|
|
45
|
+
* @param {{checkJs?: boolean, build?: boolean}} [options]
|
|
46
|
+
* @returns {PreCommitPlan}
|
|
47
|
+
*/
|
|
48
|
+
export function planPreCommit(staged, options = {}) {
|
|
49
|
+
const formatAll = staged.some((file) => FORMAT_CONFIG.test(file))
|
|
50
|
+
const lintAll = staged.some((file) => LINT_CONFIG.test(file))
|
|
51
|
+
const pythonAll = staged.some((file) => PYTHON_CONFIG.test(file))
|
|
52
|
+
const formatFiles = staged.filter((file) => FORMATTABLE.test(file))
|
|
53
|
+
const lintFiles = staged.filter((file) => LINTABLE.test(file))
|
|
54
|
+
const pythonFiles = staged.filter((file) => PYTHON.test(file))
|
|
55
|
+
const typeCheck = staged.some(
|
|
56
|
+
(file) => TYPED.test(file) || (options.checkJs === true && JAVASCRIPT.test(file))
|
|
57
|
+
)
|
|
58
|
+
const rust = staged.some((file) => RUST.test(file))
|
|
59
|
+
const code =
|
|
60
|
+
typeCheck ||
|
|
61
|
+
lintFiles.length > 0 ||
|
|
62
|
+
pythonFiles.length > 0 ||
|
|
63
|
+
rust ||
|
|
64
|
+
staged.some((file) => /(?:^|\/)package\.json$/.test(file))
|
|
65
|
+
return {
|
|
66
|
+
formatAll,
|
|
67
|
+
formatFiles,
|
|
68
|
+
lintAll,
|
|
69
|
+
lintFiles,
|
|
70
|
+
typeCheck,
|
|
71
|
+
pythonAll,
|
|
72
|
+
pythonFiles,
|
|
73
|
+
rust,
|
|
74
|
+
build: options.build === true && code,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse `git diff --cached --name-only -z` output.
|
|
80
|
+
* @param {string} output
|
|
81
|
+
* @returns {string[]}
|
|
82
|
+
*/
|
|
83
|
+
export function parseStagedFiles(output) {
|
|
84
|
+
return output.split('\0').filter(Boolean)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the `pre_commit_build` configuration value. Building at commit time
|
|
89
|
+
* is opt-in: it serializes parallel work on a shared checkout and CI rebuilds
|
|
90
|
+
* every change anyway.
|
|
91
|
+
* @param {string|undefined} value
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
export function preCommitBuildEnabled(value) {
|
|
95
|
+
const resolved = value === undefined || value === '' ? 'false' : value
|
|
96
|
+
if (resolved !== 'true' && resolved !== 'false')
|
|
97
|
+
throw new Error(`Unsupported pre_commit_build: ${resolved}; use true or false.`)
|
|
98
|
+
return resolved === 'true'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Split a file list so one tool invocation never exceeds the platform's
|
|
103
|
+
* argument-length limit on very large commits.
|
|
104
|
+
* @param {string[]} files
|
|
105
|
+
* @param {number} [size]
|
|
106
|
+
* @returns {string[][]}
|
|
107
|
+
*/
|
|
108
|
+
export function chunkFiles(files, size = 200) {
|
|
109
|
+
/** @type {string[][]} */
|
|
110
|
+
const chunks = []
|
|
111
|
+
for (let index = 0; index < files.length; index += size)
|
|
112
|
+
chunks.push(files.slice(index, index + size))
|
|
113
|
+
return chunks
|
|
114
|
+
}
|
package/src/runtime-core.mjs
CHANGED
|
@@ -20,6 +20,12 @@ import { docsOnlyPullRequest } from './lib/docs-only.mjs'
|
|
|
20
20
|
import { readTaskFilters, resolveChangedPaths, taskAffected } from './lib/task-filters.mjs'
|
|
21
21
|
import { classifyValidationMode, evaluateValidationGate } from './lib/validation-policy.mjs'
|
|
22
22
|
import { readReleaseConfig, validateGeneratedReleaseDiff } from './lib/release-policy.mjs'
|
|
23
|
+
import {
|
|
24
|
+
chunkFiles,
|
|
25
|
+
parseStagedFiles,
|
|
26
|
+
planPreCommit,
|
|
27
|
+
preCommitBuildEnabled,
|
|
28
|
+
} from './lib/pre-commit.mjs'
|
|
23
29
|
import { runNodePackagePerformance } from './lib/node-package-performance.mjs'
|
|
24
30
|
import {
|
|
25
31
|
EVAL_BUDGET_FILE_DEFAULT,
|
|
@@ -1167,35 +1173,89 @@ function printProfile(command) {
|
|
|
1167
1173
|
writeOutput('npm_publish', config.npm_publish ?? 'false')
|
|
1168
1174
|
}
|
|
1169
1175
|
|
|
1176
|
+
/** Whether JavaScript sources are type-checked (a root jsconfig or `checkJs`). */
|
|
1177
|
+
function checksJavascript() {
|
|
1178
|
+
if (existsSync(resolve(root, 'jsconfig.json'))) return true
|
|
1179
|
+
return /"checkJs"\s*:\s*true/.test(readFileSafe(resolve(root, 'tsconfig.json')) ?? '')
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** @param {string} tool @param {string[]} args @param {string[]} files */
|
|
1183
|
+
function runToolOnFiles(tool, args, files) {
|
|
1184
|
+
for (const chunk of chunkFiles(files)) runTool(tool, [...args, ...chunk])
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1170
1187
|
/**
|
|
1171
1188
|
* The local commit gate.
|
|
1172
1189
|
*
|
|
1173
1190
|
* Exported so the generated `.githooks/pre-commit` can reach it: the hook is
|
|
1174
|
-
* what turns this into an actual gate
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1191
|
+
* what turns this into an actual gate. The generated hook is the
|
|
1192
|
+
* consumer-visible contract, so keep the two in step.
|
|
1193
|
+
*
|
|
1194
|
+
* The gate is change-aware: it formats and lints only staged files, runs the
|
|
1195
|
+
* project's type-check command only when typed sources or compiler config are
|
|
1196
|
+
* staged, and never builds unless `pre_commit_build: true` opts back in. The
|
|
1197
|
+
* full suite still runs in CI. Tools see working-tree contents, so a partially
|
|
1198
|
+
* staged file is checked as it exists on disk.
|
|
1177
1199
|
*/
|
|
1178
1200
|
export function preCommit() {
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1201
|
+
const staged = parseStagedFiles(
|
|
1202
|
+
capture('git', ['diff', '--cached', '--name-only', '-z', '--diff-filter=ACMR'])
|
|
1203
|
+
)
|
|
1204
|
+
// Deletions still go through the whitespace guard; they need no tool run.
|
|
1205
|
+
if (!staged.length && !capture('git', ['diff', '--cached', '--name-only'])) return
|
|
1181
1206
|
const check = spawnSync('git', ['diff', '--cached', '--check'], { cwd: root, stdio: 'inherit' })
|
|
1182
1207
|
if (check.status !== 0) process.exit(check.status ?? 1)
|
|
1183
|
-
if (
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1208
|
+
if (!staged.length) return
|
|
1209
|
+
|
|
1210
|
+
const plan = planPreCommit(staged, {
|
|
1211
|
+
checkJs: checksJavascript(),
|
|
1212
|
+
build: preCommitBuildEnabled(config.pre_commit_build),
|
|
1213
|
+
})
|
|
1214
|
+
const js = (hasLanguage('typescript') || hasLanguage('javascript')) && hasRootJavascriptProject()
|
|
1215
|
+
|
|
1216
|
+
if (js && (plan.formatAll || plan.formatFiles.length)) {
|
|
1217
|
+
if (plan.formatAll) {
|
|
1218
|
+
if (!runScript(['format:check', 'format', 'fmt']) && hasOxfmtSetup())
|
|
1219
|
+
runTool('oxfmt', ['--check', '.'])
|
|
1220
|
+
} else if (hasOxfmtSetup()) {
|
|
1221
|
+
runToolOnFiles('oxfmt', ['--check', '--no-error-on-unmatched-pattern'], plan.formatFiles)
|
|
1222
|
+
} else if (plan.formatFiles.some((file) => /\.(?:[cm]?[jt]sx?|json|mdx?|ya?ml)$/.test(file))) {
|
|
1223
|
+
// A repository-owned formatter cannot be scoped generically; its script
|
|
1224
|
+
// still runs, but only when a file it plausibly owns is staged.
|
|
1225
|
+
runScript(['format:check', 'format', 'fmt'])
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (js && (plan.lintAll || plan.lintFiles.length)) {
|
|
1229
|
+
if (plan.lintAll) {
|
|
1230
|
+
if (!runScript(['lint']) && hasOxlintSetup()) runTool('oxlint', ['--deny-warnings'])
|
|
1231
|
+
} else if (hasOxlintSetup()) {
|
|
1232
|
+
runToolOnFiles(
|
|
1233
|
+
'oxlint',
|
|
1234
|
+
['--deny-warnings', '--no-error-on-unmatched-pattern'],
|
|
1235
|
+
plan.lintFiles
|
|
1236
|
+
)
|
|
1237
|
+
} else {
|
|
1238
|
+
runScript(['lint'])
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (js && plan.typeCheck) {
|
|
1242
|
+
// Use the project's own command so project references, workspaces, and
|
|
1243
|
+
// path aliases resolve exactly as they do in CI.
|
|
1244
|
+
const scripted = runScript(['type-check', 'typecheck', 'type:check'])
|
|
1245
|
+
if (!scripted && existsSync(resolve(root, 'tsconfig.json'))) runTool('tsc', ['--noEmit'])
|
|
1190
1246
|
}
|
|
1191
|
-
if (
|
|
1247
|
+
if (plan.rust) {
|
|
1192
1248
|
run('cargo', ['fmt', '--check'])
|
|
1193
1249
|
run('cargo', ['clippy', '--all-targets', '--', '-D', 'warnings'])
|
|
1194
1250
|
}
|
|
1195
|
-
if (
|
|
1251
|
+
if (plan.pythonAll) {
|
|
1196
1252
|
runTool('ruff', ['format', '--check', '.'])
|
|
1197
1253
|
runTool('ruff', ['check', '.'])
|
|
1254
|
+
} else if (plan.pythonFiles.length) {
|
|
1255
|
+
runToolOnFiles('ruff', ['format', '--check', '--force-exclude'], plan.pythonFiles)
|
|
1256
|
+
runToolOnFiles('ruff', ['check', '--force-exclude'], plan.pythonFiles)
|
|
1198
1257
|
}
|
|
1258
|
+
if (plan.build) ci('build')
|
|
1199
1259
|
}
|
|
1200
1260
|
|
|
1201
1261
|
// Only dispatch when this module is the process entry point. The CLI imports
|