@zgeoff/chex 0.0.4 → 0.0.5

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
@@ -52,14 +52,14 @@ The docs checks read the repo's READMEs, its `docs/` tree and its skills:
52
52
 
53
53
  The repo checks read the branch, the pushed commits and the repo's config:
54
54
 
55
- | Check | Fails on | Arguments |
56
- | ------------------ | --------------------------------------------------------------------------- | --------------- |
57
- | `commits` | a pushed commit whose message fails `@zgeoff/commitlint-config` | `--pushed-refs` |
58
- | `local-paths` | a line a pushed commit adds that holds the home folder or the host name | `--pushed-refs` |
59
- | `test-timeouts` | a `bun test` timeout above its lane's limit, in a script or a `bunfig.toml` | none |
60
- | `workflows` | nothing: it warns on each fault of the shared workflow lint, and passes | the paths |
61
- | `dependency-gates` | a stale age-gate exclude, an override or an audit ignore that nothing needs | none |
62
- | `uncommitted` | a file the branch changes that holds a change the push does not send | none |
55
+ | Check | Fails on | Arguments |
56
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | --------------- |
57
+ | `commits` | a pushed commit whose message fails `@zgeoff/commitlint-config` | `--pushed-refs` |
58
+ | `local-paths` | a line a pushed commit adds that holds the home folder or the host name | `--pushed-refs` |
59
+ | `test-timeouts` | a `bun test` timeout above its lane's limit, in a script or a `bunfig.toml` | none |
60
+ | `workflows` | nothing: it warns on each fault of the shared workflow lint, and passes | the paths |
61
+ | `dependency-gates` | a stale age-gate exclude, an override or an audit ignore that nothing needs, or a first-party package with no age-gate exclude | none |
62
+ | `uncommitted` | a file the branch changes that holds a change the push does not send | none |
63
63
 
64
64
  With no path, `prose` and `links` read every README outside `.claude/` and `.agents/`, every file
65
65
  under `docs/`, each spike's `results.md`, and the Markdown under `.agents/skills/`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/chex",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Run each shared docs and repo check as one subcommand, with the lefthook pre-push baseline that calls them",
5
5
  "keywords": [
6
6
  "checks",
@@ -0,0 +1,24 @@
1
+ import { firstPartyScopes } from './first-party-scopes.ts';
2
+ import type { AgeGate } from './parse-age-gate.ts';
3
+ import type { DependencyGateFinding } from './types.ts';
4
+
5
+ // An exclude with a version, such as `@zgeoff/std@1.0.0`, lets only that release through, so it
6
+ // does not count.
7
+ export function collectMissingFirstPartyExcludes(
8
+ ageGate: AgeGate,
9
+ registryNames: readonly string[],
10
+ ): DependencyGateFinding[] {
11
+ if (!ageGate.isOn) {
12
+ return [];
13
+ }
14
+
15
+ return registryNames
16
+ .filter((name) => firstPartyScopes.some((scope) => name.startsWith(scope)))
17
+ .filter((name) => !ageGate.excludes.includes(name))
18
+ .map((name) => ({
19
+ path: 'bunfig.toml',
20
+ entry: name,
21
+ problem:
22
+ 'the first-party package has no age-gate exclude, so bun install refuses each release younger than the gate',
23
+ }));
24
+ }
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { firstPartyScopes } from './first-party-scopes.ts';
3
- import { isRecord } from './is-record.ts';
3
+ import { parseAgeGate } from './parse-age-gate.ts';
4
4
  import type { DependencyGateFinding } from './types.ts';
5
5
 
6
6
  export function collectStaleAgeGateExcludes(
@@ -9,8 +9,8 @@ export function collectStaleAgeGateExcludes(
9
9
  ): DependencyGateFinding[] {
10
10
  const removalDates = collectRemovalDates(bunfig);
11
11
 
12
- return parseAgeGateExcludes(bunfig)
13
- .filter((name) => !firstPartyScopes.some((scope) => name.startsWith(scope)))
12
+ return parseAgeGate(bunfig)
13
+ .excludes.filter((name) => !firstPartyScopes.some((scope) => name.startsWith(scope)))
14
14
  .flatMap((name) => {
15
15
  const removalDate = removalDates.get(name);
16
16
 
@@ -61,23 +61,3 @@ function collectRemovalDates(bunfig: string): Map<string, string> {
61
61
 
62
62
  return removalDates;
63
63
  }
64
-
65
- function parseAgeGateExcludes(bunfig: string): string[] {
66
- const parsed: unknown = Bun.TOML.parse(bunfig);
67
- const install = isRecord(parsed) ? parsed['install'] : undefined;
68
- const excludes = isRecord(install) ? install['minimumReleaseAgeExcludes'] : undefined;
69
-
70
- if (excludes === undefined) {
71
- return [];
72
- }
73
-
74
- if (!isStringList(excludes)) {
75
- throw new TypeError('bunfig.toml sets a minimumReleaseAgeExcludes that is not a list of names');
76
- }
77
-
78
- return excludes;
79
- }
80
-
81
- function isStringList(value: unknown): value is string[] {
82
- return Array.isArray(value) && value.every((item: unknown) => typeof item === 'string');
83
- }
@@ -1 +1 @@
1
- export const firstPartyScopes = ['@zgeoff/', '@impz/'] as const;
1
+ export const firstPartyScopes = ['@zgeoff/', '@impz/', '@wringer/'] as const;
@@ -0,0 +1,26 @@
1
+ import { isRecord } from './is-record.ts';
2
+
3
+ export interface AgeGate {
4
+ readonly isOn: boolean;
5
+ readonly excludes: readonly string[];
6
+ }
7
+
8
+ export function parseAgeGate(bunfig: string): AgeGate {
9
+ const parsed: unknown = Bun.TOML.parse(bunfig);
10
+ const install = isRecord(parsed) ? parsed['install'] : undefined;
11
+ const minimumReleaseAge = isRecord(install) ? install['minimumReleaseAge'] : undefined;
12
+ const excludes = isRecord(install) ? install['minimumReleaseAgeExcludes'] : undefined;
13
+
14
+ if (excludes !== undefined && !isStringList(excludes)) {
15
+ throw new TypeError('bunfig.toml sets a minimumReleaseAgeExcludes that is not a list of names');
16
+ }
17
+
18
+ return {
19
+ isOn: minimumReleaseAge !== undefined && minimumReleaseAge !== 0 && minimumReleaseAge !== '0',
20
+ excludes: excludes ?? [],
21
+ };
22
+ }
23
+
24
+ function isStringList(value: unknown): value is string[] {
25
+ return Array.isArray(value) && value.every((item: unknown) => typeof item === 'string');
26
+ }
@@ -0,0 +1,27 @@
1
+ import { isRecord } from './is-record.ts';
2
+
3
+ // An entry's id is `<name>@<version>`. A workspace, link, file or git entry carries a protocol in
4
+ // its version, such as `workspace:packages/std`, and bun never puts it through the age gate.
5
+ export function parseLockfileRegistryNames(lockfile: string): string[] {
6
+ const parsed: unknown = Bun.JSONC.parse(lockfile);
7
+ const packages = isRecord(parsed) ? parsed['packages'] : undefined;
8
+
9
+ if (!isRecord(packages)) {
10
+ throw new TypeError('bun.lock has no packages object');
11
+ }
12
+
13
+ const names = Object.values(packages).flatMap((entry) => {
14
+ const id: unknown = Array.isArray(entry) ? entry[0] : undefined;
15
+
16
+ if (typeof id !== 'string') {
17
+ throw new TypeError('bun.lock has a package entry that does not start with its id');
18
+ }
19
+
20
+ const versionStart = id.indexOf('@', 1);
21
+ const version = id.slice(versionStart + 1);
22
+
23
+ return versionStart > 0 && !version.includes(':') ? [id.slice(0, versionStart)] : [];
24
+ });
25
+
26
+ return [...new Set(names)].toSorted();
27
+ }
@@ -1,11 +1,14 @@
1
1
  import type { RunProgram } from '../run-program.ts';
2
+ import { collectMissingFirstPartyExcludes } from './collect-missing-first-party-excludes.ts';
2
3
  import { collectStaleAgeGateExcludes } from './collect-stale-age-gate-excludes.ts';
3
4
  import { collectUnneededAuditIgnores } from './collect-unneeded-audit-ignores.ts';
4
5
  import { collectUnneededOverrides } from './collect-unneeded-overrides.ts';
6
+ import { parseAgeGate } from './parse-age-gate.ts';
5
7
  import { parseAuditAdvisories } from './parse-audit-advisories.ts';
6
8
  import { parseAuditIgnoreIDs } from './parse-audit-ignore-ids.ts';
7
9
  import { parseFixablePackages } from './parse-fixable-packages.ts';
8
10
  import { parseLockfileDependents } from './parse-lockfile-dependents.ts';
11
+ import { parseLockfileRegistryNames } from './parse-lockfile-registry-names.ts';
9
12
  import type { RootManifest } from './parse-root-manifest.ts';
10
13
  import { parseRootManifest } from './parse-root-manifest.ts';
11
14
  import type { DependencyGateFinding } from './types.ts';
@@ -36,24 +39,30 @@ export function readDependencyGateFindings(
36
39
 
37
40
  const manifest = parseRootManifest(manifestText);
38
41
  const ignoreIDs = parseAuditIgnoreIDs(manifest.auditScript);
42
+ const bunfig = sources.readRepoFile('bunfig.toml') ?? '';
43
+ const lockfile = sources.readRepoFile('bun.lock');
39
44
 
40
45
  return [
41
- ...collectStaleAgeGateExcludes(sources.readRepoFile('bunfig.toml') ?? '', sources.today),
42
- ...readOverrideFindings(sources, manifest),
46
+ ...collectStaleAgeGateExcludes(bunfig, sources.today),
47
+ ...(lockfile === null
48
+ ? []
49
+ : collectMissingFirstPartyExcludes(
50
+ parseAgeGate(bunfig),
51
+ parseLockfileRegistryNames(lockfile),
52
+ )),
53
+ ...collectOverrideFindings(manifest, lockfile),
43
54
  ...(ignoreIDs.length === 0 ? [] : readIgnoreFindings(sources, ignoreIDs)),
44
55
  ];
45
56
  }
46
57
 
47
- function readOverrideFindings(
48
- sources: Readonly<DependencyGateSources>,
58
+ function collectOverrideFindings(
49
59
  manifest: RootManifest,
60
+ lockfile: string | null,
50
61
  ): DependencyGateFinding[] {
51
62
  if (Object.keys(manifest.overrides).length === 0) {
52
63
  return [];
53
64
  }
54
65
 
55
- const lockfile = sources.readRepoFile('bun.lock');
56
-
57
66
  if (lockfile === null) {
58
67
  throw new Error('package.json sets overrides, and the repo has no bun.lock');
59
68
  }