code-foundry 1.10.0 → 1.11.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.
@@ -0,0 +1,290 @@
1
+ // @ts-check
2
+
3
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
4
+ import { spawnSync } from 'node:child_process'
5
+ import { isAbsolute, relative, resolve, sep } from 'node:path'
6
+ import { createHash } from 'node:crypto'
7
+ import { listValue, readConfig } from './config.mjs'
8
+
9
+ export const TASKS = Object.freeze([
10
+ 'format',
11
+ 'lint',
12
+ 'type_check',
13
+ 'build',
14
+ 'unit',
15
+ 'integration',
16
+ 'e2e',
17
+ 'smoke',
18
+ 'performance',
19
+ ])
20
+
21
+ /** @type {Readonly<Record<string, readonly string[]>>} */
22
+ export const TASK_SCRIPTS = Object.freeze({
23
+ format: ['format:check', 'format', 'fmt'],
24
+ lint: ['lint'],
25
+ type_check: ['type-check', 'typecheck', 'type:check'],
26
+ build: ['build'],
27
+ unit: ['test:unit', 'test:coverage', 'test'],
28
+ integration: ['test:integration'],
29
+ e2e: ['test:e2e', 'e2e'],
30
+ smoke: ['test:smoke', 'smoke'],
31
+ performance: ['performance:check', 'perf:check'],
32
+ })
33
+
34
+ /** @param {string} root */
35
+ export function readTaskPolicy(root) {
36
+ const config = readConfig(resolve(root, '.github/code-foundry.yml'))
37
+ const required = listValue(config.required_capabilities ?? '')
38
+ for (const capability of required) {
39
+ if (![...TASKS, 'coverage'].includes(capability))
40
+ throw new Error(`Unknown required capability: ${capability}`)
41
+ }
42
+ if (!['true', 'false', 'auto'].includes(config.performance ?? 'auto'))
43
+ throw new Error('performance must be true, false, or auto')
44
+ const coverageMode = config.coverage_enforcement ?? 'auto'
45
+ if (!['auto', 'required', 'off'].includes(coverageMode))
46
+ throw new Error('coverage_enforcement must be auto, required, or off')
47
+ if (required.includes('coverage') && coverageMode === 'off')
48
+ throw new Error('Required coverage cannot use coverage_enforcement: off')
49
+ if (required.includes('performance') && config.performance === 'false')
50
+ throw new Error('Required performance cannot use performance: false')
51
+ if (config.performance === 'true' && !required.includes('performance'))
52
+ required.push('performance')
53
+ const coverageRequired = required.includes('coverage') || coverageMode === 'required'
54
+ if (coverageRequired && !required.includes('unit')) required.push('unit')
55
+ const minimum = Number(config.coverage_minimum ?? '80')
56
+ if (!Number.isFinite(minimum) || minimum < 0 || minimum > 100)
57
+ throw new Error('coverage_minimum must be a finite percentage between 0 and 100')
58
+ const metrics = listValue(config.coverage_metrics ?? 'lines')
59
+ if (
60
+ !metrics.length ||
61
+ metrics.some(
62
+ (metricName) => !['lines', 'functions', 'branches', 'statements'].includes(metricName)
63
+ )
64
+ )
65
+ throw new Error('coverage_metrics must select lines, functions, branches, or statements')
66
+ return { config, required, coverageMode, coverageRequired, minimum, metrics }
67
+ }
68
+
69
+ /** @param {string} root @returns {Record<string, any>} */
70
+ export function readTaskPackage(root) {
71
+ const file = resolve(root, 'package.json')
72
+ if (!existsSync(file)) return {}
73
+ const value = JSON.parse(readFileSync(file, 'utf8'))
74
+ if (!value || typeof value !== 'object' || Array.isArray(value))
75
+ throw new Error('package.json must contain an object')
76
+ return value
77
+ }
78
+
79
+ /**
80
+ * Explain the core's applicability decision and reject known native no-ops.
81
+ * Discovery is not execution; a native command may still fail at run time.
82
+ * @param {string} root
83
+ * @param {string} task
84
+ * @param {Record<string, string>} profile
85
+ */
86
+ export function describeTask(root, task, profile) {
87
+ if (!TASKS.includes(task)) throw new Error(`Unknown CI task: ${task}`)
88
+ const policy = readTaskPolicy(root)
89
+ const pkg = readTaskPackage(root)
90
+ const script = TASK_SCRIPTS[task]?.find((name) => {
91
+ const value = pkg.scripts?.[name]
92
+ return typeof value === 'string' && value.trim().length > 0
93
+ })
94
+ const required = policy.required.includes(task)
95
+ let applicable = profile.applicable === 'true'
96
+ let reason = applicable ? 'native runtime discovery' : 'no supported entrypoint was discovered'
97
+ if (script && applicable) reason = `package-script:${script}`
98
+ if (applicable && !script && ['format', 'lint', 'type_check', 'build'].includes(task)) {
99
+ const rust = profile.rust === 'true' && existsSync(resolve(root, 'Cargo.toml'))
100
+ const python =
101
+ profile.python === 'true' &&
102
+ (existsSync(resolve(root, 'pyproject.toml')) ||
103
+ existsSync(resolve(root, 'requirements.txt')) ||
104
+ existsSync(resolve(root, 'uv.lock')))
105
+ const js = profile.javascript === 'true' && existsSync(resolve(root, 'package.json'))
106
+ const native =
107
+ rust ||
108
+ (['format', 'lint'].includes(task) && python) ||
109
+ (task === 'type_check' && existsSync(resolve(root, 'tsconfig.json'))) ||
110
+ (js && task === 'format' && hasNativeToolSetup(root, pkg, 'oxfmt')) ||
111
+ (js && task === 'lint' && hasNativeToolSetup(root, pkg, 'oxlint'))
112
+ if (!native) {
113
+ applicable = false
114
+ reason = 'repository detected, but no executable script or supported native fallback exists'
115
+ }
116
+ }
117
+ if (required && !applicable) throw new Error(`Required capability ${task}: ${reason}`)
118
+ return {
119
+ task,
120
+ applicable,
121
+ required,
122
+ reason,
123
+ source: script ? `package-script:${script}` : 'runtime',
124
+ }
125
+ }
126
+
127
+ /** @param {string} root @returns {string[]} */
128
+ function repositoryFiles(root) {
129
+ const result = spawnSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' })
130
+ return result.status === 0 ? result.stdout.split(/\r?\n/).filter(Boolean) : []
131
+ }
132
+
133
+ /** @param {string} root @param {Record<string, any>} pkg @param {'oxfmt'|'oxlint'} tool */
134
+ function hasNativeToolSetup(root, pkg, tool) {
135
+ const dependencies = {
136
+ ...pkg.dependencies,
137
+ ...pkg.devDependencies,
138
+ ...pkg.optionalDependencies,
139
+ ...pkg.peerDependencies,
140
+ }
141
+ if (dependencies[tool]) return true
142
+ if (
143
+ Object.values(pkg.scripts ?? {}).some((value) =>
144
+ new RegExp(`\\b${tool}\\b`).test(String(value))
145
+ )
146
+ )
147
+ return true
148
+ const config =
149
+ tool === 'oxfmt'
150
+ ? /(^|\/)(\.oxfmtrc\.json|oxfmt\.config\.[^/]*)$/
151
+ : /(^|\/)(\.oxlintrc\.json|oxlint\.config\.[^/]*)$/
152
+ return repositoryFiles(root).some((file) => config.test(file))
153
+ }
154
+
155
+ /** Repository-owned evidence must never escape the checkout through paths or symlinks.
156
+ * @param {string} root @param {string} file
157
+ */
158
+ export function evidencePath(root, file) {
159
+ if (!file || isAbsolute(file)) throw new Error('Evidence paths must be repository-relative')
160
+ const base = realpathSync(root)
161
+ const target = resolve(base, file)
162
+ const inside = relative(base, target)
163
+ if (inside === '..' || inside.startsWith(`..${sep}`))
164
+ throw new Error(`Evidence path escapes repository: ${file}`)
165
+
166
+ // Resolve the deepest existing ancestor so a missing report cannot hide
167
+ // behind a symlinked directory outside the checkout.
168
+ let existing = target
169
+ while (!existsSync(existing)) {
170
+ const parent = resolve(existing, '..')
171
+ if (parent === existing) break
172
+ existing = parent
173
+ }
174
+ const actual = relative(base, realpathSync(existing))
175
+ if (actual === '..' || actual.startsWith(`..${sep}`))
176
+ throw new Error(`Evidence symlink escapes repository: ${file}`)
177
+ if (existsSync(target) && !statSync(target).isFile())
178
+ throw new Error(`Evidence must be a regular file: ${file}`)
179
+ return target
180
+ }
181
+
182
+ /** @param {string} root @param {ReturnType<typeof readTaskPolicy>} policy */
183
+ export function coverageFiles(root, policy) {
184
+ const configured = listValue(policy.config.coverage_report ?? '')
185
+ return (
186
+ configured.length ? configured : ['coverage/coverage-summary.json', 'coverage/lcov.info']
187
+ ).map((file) => ({ file, path: evidencePath(root, file) }))
188
+ }
189
+
190
+ /** @param {string} path */
191
+ export function fingerprint(path) {
192
+ if (!existsSync(path)) return null
193
+ const stat = statSync(path)
194
+ return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:${createHash('sha256').update(readFileSync(path)).digest('hex')}`
195
+ }
196
+
197
+ /** @param {number} total @param {number} covered @param {string} label */
198
+ function metric(total, covered, label) {
199
+ if (
200
+ !Number.isSafeInteger(total) ||
201
+ !Number.isSafeInteger(covered) ||
202
+ total < 0 ||
203
+ covered < 0 ||
204
+ covered > total
205
+ )
206
+ throw new Error(`Invalid coverage counts for ${label}`)
207
+ return { total, covered, percent: total ? (covered * 100) / total : null }
208
+ }
209
+
210
+ /** @param {string} content @param {'json'|'lcov'} format */
211
+ export function parseCoverage(content, format) {
212
+ /** @type {Record<string, {total: number, covered: number, percent: number|null}>} */
213
+ const metrics = {}
214
+ if (format === 'json') {
215
+ const report = JSON.parse(content)
216
+ if (!report?.total || typeof report.total !== 'object')
217
+ throw new Error('Coverage summary is missing total')
218
+ for (const name of ['lines', 'functions', 'branches', 'statements']) {
219
+ if (report.total[name])
220
+ metrics[name] = metric(report.total[name].total, report.total[name].covered, name)
221
+ }
222
+ } else {
223
+ const fields = { lines: ['LF', 'LH'], functions: ['FNF', 'FNH'], branches: ['BRF', 'BRH'] }
224
+ const records = content.split(/end_of_record\s*(?:\r?\n|$)/).filter((record) => record.trim())
225
+ if (!records.length) throw new Error('LCOV report contains no records')
226
+ for (const record of records) {
227
+ if (!/^SF:.+/m.test(record)) throw new Error('LCOV record is missing SF')
228
+ for (const [name, [totalKey, hitKey]] of Object.entries(fields)) {
229
+ const totalMatch = record.match(new RegExp(`^${totalKey}:(\\d+)\\r?$`, 'm'))
230
+ const hitMatch = record.match(new RegExp(`^${hitKey}:(\\d+)\\r?$`, 'm'))
231
+ if (!totalMatch && !hitMatch) continue
232
+ if (!totalMatch || !hitMatch) throw new Error(`Incomplete LCOV ${name} counts`)
233
+ const current = metric(Number(totalMatch[1]), Number(hitMatch[1]), name)
234
+ const previous = metrics[name] ?? { total: 0, covered: 0 }
235
+ metrics[name] = metric(
236
+ previous.total + current.total,
237
+ previous.covered + current.covered,
238
+ name
239
+ )
240
+ }
241
+ }
242
+ }
243
+ if (!metrics.lines?.total) throw new Error('Coverage report measured no lines')
244
+ return metrics
245
+ }
246
+
247
+ /**
248
+ * Auto enforces present reports but reports absence explicitly. Required mode also
249
+ * rejects absent/stale reports. Reports are never deleted to manufacture freshness.
250
+ * @param {string} root
251
+ * @param {ReturnType<typeof readTaskPolicy>} policy
252
+ * @param {Record<string, string|null>} before
253
+ */
254
+ export function evaluateCoverage(root, policy, before) {
255
+ if (policy.coverageMode === 'off')
256
+ return { status: 'skipped', reason: 'coverage enforcement explicitly disabled', artifacts: [] }
257
+ const files = coverageFiles(root, policy).filter(({ path }) => existsSync(path))
258
+ if (!files.length) {
259
+ if (policy.coverageRequired) throw new Error('Required coverage report was not produced')
260
+ return {
261
+ status: 'skipped',
262
+ reason: 'no coverage report; set coverage_enforcement: required to require evidence',
263
+ artifacts: [],
264
+ }
265
+ }
266
+ const reports = files.map(({ file, path }) => {
267
+ if (fingerprint(path) === before[file])
268
+ throw new Error(`Coverage report was not refreshed by this run: ${file}`)
269
+ const metrics = parseCoverage(
270
+ readFileSync(path, 'utf8'),
271
+ file.endsWith('.info') ? 'lcov' : 'json'
272
+ )
273
+ for (const name of policy.metrics) {
274
+ const value = metrics[name]
275
+ if (!value || value.percent === null)
276
+ throw new Error(`Coverage report has no measured ${name}: ${file}`)
277
+ if (value.percent < policy.minimum)
278
+ throw new Error(
279
+ `${name} coverage ${value.percent.toFixed(2)}% is below ${policy.minimum}% (${file})`
280
+ )
281
+ }
282
+ return { file, metrics }
283
+ })
284
+ return {
285
+ status: 'passed',
286
+ reason: 'fresh coverage meets configured thresholds',
287
+ artifacts: files.map(({ file }) => file),
288
+ reports,
289
+ }
290
+ }