From 0b701c79c3c2d7035fdbc9a9fcb0430ab0dc56ad Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 29 Oct 2026 13:21:02 +0000 Subject: [PATCH] build: depend on combinatrix 2 combinatrix 2 types a dimension's values as a non-empty list, so the measured lists are typed non-empty, and the measurement docs say the variant matrix is generated from the settings schema. --- docs/headroom-measurement.md | 2 +- package.json | 2 +- pnpm-lock.yaml | 28 +++++++++++++++++++++++----- pnpm-workspace.yaml | 3 ++- scripts/headroom-measure-core.ts | 11 +++++++---- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/docs/headroom-measurement.md b/docs/headroom-measurement.md index d3242604..76e77942 100644 --- a/docs/headroom-measurement.md +++ b/docs/headroom-measurement.md @@ -34,7 +34,7 @@ The multipliers are the documented ones (https://platform.claude.com/docs/en/bui ## Variants -The matrix covers `cache` (headroom's default) and `token` modes, lower `--target-ratio` values, `--lossless` and `--no-ccr` (retrieval markers off), and the experimental `--intercept-tool-results` and `--read-maturation` behind their rollout channels. `--variant NAME` (repeatable) selects by name, and an unknown name is an error. +The matrix covers `cache` (headroom's default) and `token` modes, lower `--target-ratio` values, `--lossless` and `--no-ccr` (retrieval markers off), and the experimental `--intercept-tool-results` and `--read-maturation` behind their rollout channels. The matrix is generated, not listed: [combinatrix](https://www.npmjs.com/package/combinatrix) enumerates every combination of those settings that headroom's settings schema accepts, so a rule added to the schema changes the matrix with no edit here, and the rollout channel each experimental feature needs is derived from the schema as the least channel that unlocks it. `--variant NAME` (repeatable) selects by name, and an unknown name is an error. ## Limits diff --git a/package.json b/package.json index 44c73a6a..350436fd 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@types/node": "^26.5.1", "@types/picomatch": "^4.0.3", "@vitest/coverage-v8": "^4.1.12", - "combinatrix": "1.0.0", + "combinatrix": "2.0.1", "conventional-changelog-conventionalcommits": "^10.4.0", "esbuild": "^0.28.2", "eslint": "^10.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d1c19cd..e73ded7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,8 +244,8 @@ importers: specifier: ^4.1.12 version: 4.1.12(vitest@4.1.12) combinatrix: - specifier: 1.0.0 - version: 1.0.0 + specifier: 2.0.1 + version: 2.0.1 conventional-changelog-conventionalcommits: specifier: ^10.4.0 version: 10.4.1 @@ -1915,8 +1915,8 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combinatrix@1.0.0: - resolution: {integrity: sha512-lk48dsHFsJMWbv0BPDNFSG9aYETJd0MEKGyHLRYKayLbABRqdhu4EWhSsGekpCTpSddhgcynH8EfnoVtM+QH2w==} + combinatrix@2.0.1: + resolution: {integrity: sha512-mIPpES7RWB9VNy0w6uqSxQs9I7keMBVEI7BfiP5vPbM7PWtIaUX+yOzUmMx+lBrwPFvqMO3hqF0nA9XktaHnog==} engines: {node: '>=22'} commander@15.0.0: @@ -3529,6 +3529,15 @@ packages: resolution: {integrity: sha512-OZZhg3esrKbVpJb1nXh/zBOvBz5nMihCWPLVClGMRqpV15durs3yscBR7cr3iwjTLkoTGNsgDUYILBex6PxBXA==} engines: {node: '>=20'} + trilean-regex@1.0.16: + resolution: {integrity: sha512-SGsS1IUYIuy7i6c8d5BItYqNpjdbcyP+TBbEE983V6bGGujVo7+GTiVeZkLo5T3Dh7dXMk4hIG5awgPM/Zjrlg==} + engines: {node: '>=20'} + + trilean@1.11.1: + resolution: {integrity: sha512-hWwxFZLaM1OeJv8e/9R+FwnsJPfsWss01Kvg+kZqADFOtvaCVBh2dE4ZHSq8nDsUWvNi+WgcpTmP0xauvXQdcg==} + engines: {node: '>=20'} + hasBin: true + trilean@1.9.0: resolution: {integrity: sha512-H0vIf/mAcEs3xvlItoaisdW5GtslufJS9VKTnt0IWzps+HOM0OO2E3hHqTFoEpGbJ3MisbjdQKSUmy+EiSzhgQ==} engines: {node: '>=20'} @@ -5329,7 +5338,9 @@ snapshots: color-name@1.1.4: {} - combinatrix@1.0.0: {} + combinatrix@2.0.1: + dependencies: + trilean: 1.11.1 commander@15.0.0: {} @@ -6977,6 +6988,13 @@ snapshots: trilean-regex@1.0.13: {} + trilean-regex@1.0.16: {} + + trilean@1.11.1: + dependencies: + trilean-regex: 1.0.16 + zod: 4.6.7 + trilean@1.9.0: dependencies: trilean-regex: 1.0.13 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b7001539..591c77fa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,7 +3,8 @@ allowBuilds: minimumReleaseAgeExclude: - '@exadev/eslint-config@2.12.1' - - 'combinatrix@1.0.0' + - combinatrix + - trilean # @semantic-release/release-notes-generator pins conventional-changelog-writer to # ^8.0.0, which predates the breaking Handlebars-to-render-function template rewrite that conventional-changelog-conventionalcommits@10.x now requires -- without this override, generateNotes silently produces an empty changelog body (just the version header, no Features/Bug Fixes sections). Confirmed upstream: https://github.com/semantic-release/release-notes-generator/issues/992 diff --git a/scripts/headroom-measure-core.ts b/scripts/headroom-measure-core.ts index 3692aa24..ab02495b 100644 --- a/scripts/headroom-measure-core.ts +++ b/scripts/headroom-measure-core.ts @@ -468,19 +468,22 @@ export function variantName(settings: Readonly): string { return parts.length === 1 && settings.mode === undefined ? `${HEADROOM_DEFAULT_MODE} (default)` : parts.join(", "); } +/** A list of at least one value, as combinatrix's `choice` takes it: every measured list starts with the unset case, so none is ever empty. */ +type NonEmptyList = readonly [T, ...T[]]; + /** An optional feature is either left unset or switched on. */ -const FEATURE_STATES = [undefined, true]; +const FEATURE_STATES: NonEmptyList = [undefined, true]; /** A setting's measured values when its default is the unset case: unset, then every value but the default. */ -function unsetOrNonDefault(values: readonly V[], defaultValue: V): (V | undefined)[] { +function unsetOrNonDefault(values: readonly V[], defaultValue: V): NonEmptyList { return [undefined, ...values.filter((value) => value !== defaultValue)]; } /** The measured values of each free setting, and the rollout channels a feature may need. */ const MEASURED_MODES = unsetOrNonDefault(HEADROOM_MODES, HEADROOM_DEFAULT_MODE); -const MEASURED_RATIOS = [undefined, ...MEASURED_TARGET_RATIOS]; +const MEASURED_RATIOS: NonEmptyList = [undefined, ...MEASURED_TARGET_RATIOS]; const MEASURED_CCR_MODES = unsetOrNonDefault(HEADROOM_CCR_MODES, HEADROOM_DEFAULT_CCR); -const ROLLOUT_CANDIDATES = [undefined, ...HEADROOM_ROLLOUT_CHANNELS]; +const ROLLOUT_CANDIDATES: NonEmptyList<(typeof HEADROOM_ROLLOUT_CHANNELS)[number] | undefined> = [undefined, ...HEADROOM_ROLLOUT_CHANNELS]; /** The most variants the model can hold: the product of every dimension's domain, which no constraint can enlarge. */ const VARIANT_LIMIT = MEASURED_MODES.length * MEASURED_RATIOS.length * MEASURED_CCR_MODES.length * FEATURE_STATES.length ** EXPERIMENTAL_FEATURES.length * ROLLOUT_CANDIDATES.length;