code-foundry 1.43.2 → 1.44.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/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 language-aware formatting and
48
- linting.
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
@@ -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 |
@@ -6,27 +6,37 @@ budget is exceeded. Reports are generated-run evidence and are ignored by Git.
6
6
 
7
7
  ## Enforced budgets
8
8
 
9
- | Metric | Budget | Why it is bounded |
10
- | ----------------------------------- | -------: | ---------------------------------------------------- |
11
- | CLI help startup p95 | 250 ms | Detect eager imports and startup regressions |
12
- | Runtime mode startup p95 | 250 ms | Detect baseline runtime initialization regressions |
13
- | Focused runtime tests | 10 s | Keep the representative runtime contract inexpensive |
14
- | Format, lint, type-check, and build | 15 s | Bound the local CI feedback loop |
15
- | Runtime dependencies | 0 | Keep the installed CLI dependency-free |
16
- | Development dependencies | 4 | Prevent unreviewed toolchain growth |
17
- | Packed artifact | 260 kB | Bound registry transfer and install cost |
18
- | Unpacked artifact | 1,000 kB | Bound installed footprint |
19
- | Packed files | 115 | Detect accidental release contents |
9
+ | Metric | Budget | Why it is bounded |
10
+ | ------------------------------------- | -------: | ---------------------------------------------------- |
11
+ | CLI help startup p95 | 250 ms | Detect eager imports and startup regressions |
12
+ | Runtime mode startup p95 | 750 ms | Detect baseline runtime initialization regressions |
13
+ | Focused runtime tests | 10 s | Keep the representative runtime contract inexpensive |
14
+ | Format, lint, type-check, and build | 15 s | Bound the local CI feedback loop |
15
+ | Runtime dependencies | 0 | Keep the installed CLI dependency-free |
16
+ | Development dependencies | 4 | Prevent unreviewed toolchain growth |
17
+ | Packed artifact (without changelog) | 296 kB | Bound registry transfer and install cost |
18
+ | Unpacked artifact (without changelog) | 1,136 kB | Bound installed footprint |
19
+ | Packed files (without changelog) | 120 | Detect accidental release contents |
20
20
 
21
21
  The source-of-truth budgets live in `scripts/performance-check.mjs`. When those
22
22
  limits change, update this table and include before/after measurements in the
23
- same change. The Cloudflare delivery and qualified-publication changes in this release
24
- moved the measured artifact from 990,481 to 994,555 unpacked bytes (257,384 to
25
- 258,647 packed bytes, with 112 files in both measurements). Draft protection
26
- configuration, reusable-workflow inputs, documentation, and regression tests
27
- increased the measured artifact to 998,348 unpacked bytes (259,532 packed bytes,
28
- with 112 files). The unpacked budget is therefore 1,000 kB; the packed and
29
- file-count budgets remain unchanged.
23
+ same change. Latency budgets are evaluated against the fastest of the timed
24
+ samples so a single load spike on a shared runner cannot fail the gate.
25
+
26
+ The artifact budgets measure the package **without `CHANGELOG.md`**. Release
27
+ Please prepends every release to the repository changelog, so a budget that
28
+ included it eroded on every release and repeatedly failed release pull requests.
29
+ The check packs a staging copy without the changelog and reports the repository
30
+ changelog size and its packaged (trimmed) size as informational metrics. At
31
+ v1.44.0 the measured artifact was 280,902 packed bytes, 1,070,333 unpacked bytes,
32
+ and 118 files, leaving roughly 5% packed and 6% unpacked headroom for source
33
+ growth.
34
+
35
+ The published package ships a changelog trimmed to the 20 most recent releases
36
+ with a link to the full history on GitHub. `scripts/package-changelog.mjs`
37
+ rewrites only the ephemeral checkout in the qualification pack job, immediately
38
+ before the lifecycle-free `npm pack --ignore-scripts`; the repository keeps the
39
+ complete `CHANGELOG.md` that Release Please maintains.
30
40
 
31
41
  The performance workflow disables build-cache reads and writes for this task.
32
42
  Timing comparisons therefore do not depend on a warm protected-branch cache, and
@@ -51,7 +51,9 @@ identity checks.
51
51
 
52
52
  The producer must:
53
53
 
54
- - pack the candidate with lifecycle scripts disabled;
54
+ - pack the candidate with lifecycle scripts disabled, after trimming the
55
+ checkout's `CHANGELOG.md` to recent releases with
56
+ `scripts/package-changelog.mjs`;
55
57
  - create or reuse a draft release for the exact package version;
56
58
  - attach the package archive before the release is published;
57
59
  - keep the tag at the qualified source commit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.43.2",
3
+ "version": "1.44.1",
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
+ }
@@ -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, and it used to run the whitespace
1175
- * check alone. The generated hook is the consumer-visible contract, so keep
1176
- * the two in step.
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 changed = capture('git', ['diff', '--cached', '--name-only'])
1180
- if (!changed) return
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 (/\.(js|jsx|ts|tsx|json|md|mdx|yml|yaml)$/.test(changed)) {
1184
- ci('format')
1185
- ci('lint')
1186
- // Type errors are the fastest check that still needs a full toolchain, and
1187
- // the most common reason a green-looking commit fails CI later. Run it
1188
- // here while the dependency graph is warm.
1189
- ci('type_check')
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 (/\.rs$|(^|\/)Cargo\.toml$/.test(changed)) {
1247
+ if (plan.rust) {
1192
1248
  run('cargo', ['fmt', '--check'])
1193
1249
  run('cargo', ['clippy', '--all-targets', '--', '-D', 'warnings'])
1194
1250
  }
1195
- if (/\.py$|(^|\/)(pyproject\.toml|requirements[^/]*\.txt)$/.test(changed)) {
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