@davidvornholt/standards 0.22.1 → 0.23.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/README.md CHANGED
@@ -33,7 +33,7 @@ standards poller --print-units --config <path>
33
33
 
34
34
  `check` also rejects the raw token formed by `biome-` + `ignore` anywhere in lock-listed canonical files. The intentionally blunt policy keeps synced sources compatible with consumer Biome configurations without invoking Biome or maintaining language-aware comment parsing.
35
35
 
36
- `github --check` compares the live GitHub repository (including the canonical squash-only repository settings, rulesets, and declared labels) against the merged declaration in `.github/settings.json` + `.github/settings.local.json`; `github --apply` converges that declared state. The canonical declaration enables squash while disabling merge commits and rebases, and its default-branch ruleset independently allows only squash. Undeclared live labels remain untouched, while undeclared repository-owned rulesets are removed. `check` runs the same comparison whenever `.github/settings.json` is present, and fails closed: declared state the token cannot see fails the gate with a message naming the token fix instead of passing unverified. The canonical workflow sets `STANDARDS_SKIP_GITHUB_CHECK=true` only in its unprivileged quality step because its isolated `github-settings` job runs the live comparison with its own workflow token; this is a workflow-internal integration seam, not a general way to disable the local gate. The CLI prints an explicit diagnostic whenever that exact value skips either `check`'s live phase or `github --check`; `github --apply` is never skipped. `--apply` re-diffs the PATCH response, so a setting GitHub accepts with HTTP 200 but silently ignores (plan-unavailable features) is a reported failure, not a false success. A repository whose GitHub plan cannot enforce rulesets can declare that in the seam (see `rulesetEnforcement` below); both commands then skip rulesets and plan-gated repository settings, converge the rest, and print an unprotected-branch notice on every run.
36
+ `github --check` compares the live GitHub repository (including the canonical squash-only repository settings, rulesets, and declared labels) against the merged declaration in `.github/settings.json` + `.github/settings.local.json`; `github --apply` converges that declared state. The canonical declaration enables squash while disabling merge commits and rebases, and its default-branch ruleset independently allows only squash. Undeclared live labels remain untouched, while undeclared repository-owned rulesets are removed. `check` runs the same comparison whenever `.github/settings.json` is present, and fails closed: declared state the token cannot see fails the gate with a message naming the token fix instead of passing unverified. The canonical workflow sets `STANDARDS_SKIP_GITHUB_CHECK=true` only in its unprivileged quality step because its `check` aggregator job runs the live comparison with its own workflow token; this is a workflow-internal integration seam, not a general way to disable the local gate. The CLI prints an explicit diagnostic whenever that exact value skips either `check`'s live phase or `github --check`; `github --apply` is never skipped. `--apply` re-diffs the PATCH response, so a setting GitHub accepts with HTTP 200 but silently ignores (plan-unavailable features) is a reported failure, not a false success. A repository whose GitHub plan cannot enforce rulesets can declare that in the seam (see `rulesetEnforcement` below); both commands then skip rulesets and plan-gated repository settings, converge the rest, and print an unprotected-branch notice on every run.
37
37
 
38
38
  `structure` enforces the canonical monorepo structure contract: workspace scripts (`check-types`, `lint`, `lint:fix`, `test`), root gate scripts, filtered Turbo convenience aliases, internal `0.0.0`/`workspace:*` versioning, package `exports`, shared tsconfig inheritance, and browser a11y wiring. Only a workspace containing an explicit `*.a11y.ts` suite must provide a `test:a11y` script and direct `@axe-core/playwright` and `@playwright/test` dependencies. `check` includes `structure`, so these rules gate every consumer PR.
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/cli.ts CHANGED
@@ -1216,13 +1216,13 @@ const runCheckCommand = async (consumer: string): Promise<boolean> => {
1216
1216
  };
1217
1217
 
1218
1218
  // The canonical workflow sets this only for its unprivileged quality job,
1219
- // where a separate isolated job runs the same live check with its own workflow
1219
+ // where the check aggregator job runs the same live check with its own workflow
1220
1220
  // token. Absent that exact workflow seam, local and explicit checks remain
1221
1221
  // fail-closed.
1222
1222
  const runGithubCheckGate = (consumer: string): Promise<boolean> => {
1223
1223
  if (process.env[SKIP_GITHUB_CHECK_ENV] === 'true') {
1224
1224
  console.log(
1225
- `standards github: live settings check skipped because ${SKIP_GITHUB_CHECK_ENV}=true; the canonical workflow's isolated github-settings job must own this check`,
1225
+ `standards github: live settings check skipped because ${SKIP_GITHUB_CHECK_ENV}=true; the canonical workflow's check aggregator job must own this check`,
1226
1226
  );
1227
1227
  return Promise.resolve(true);
1228
1228
  }
@@ -35,7 +35,7 @@ import {
35
35
  import { type GithubSettings, isRecord } from './github-settings-parse';
36
36
 
37
37
  const LABEL_VISIBILITY_PROBLEM =
38
- 'declared labels not visible to this token, so the gate cannot verify them. Label reads need issues: read (or pull-requests: read); the canonical github-settings job already grants issues: read, so in CI this points at a stale synced workflow or an organization policy restricting the workflow token rather than anything to change in the declaration. Locally, use a token with one of those permissions';
38
+ 'declared labels not visible to this token, so the gate cannot verify them. Label reads need issues: read (or pull-requests: read); the canonical check aggregator job already grants issues: read, so in CI this points at a stale synced workflow or an organization policy restricting the workflow token rather than anything to change in the declaration. Locally, use a token with one of those permissions';
39
39
  const PERMISSION_DENIAL_MESSAGES: ReadonlySet<string> = new Set([
40
40
  'Resource not accessible by integration',
41
41
  'Resource not accessible by personal access token',
@@ -54,9 +54,11 @@ const REVIEW_STATUSES: ReadonlySet<string> = new Set([
54
54
  'cannot-review',
55
55
  ]);
56
56
 
57
- // Conventional Commit subject: consumers lint PR titles in CI, so a
58
- // malformed title is caught here instead of as a red check later.
59
- const PR_TITLE_PATTERN = /^[a-z]+(?:\([^)]+\))?!?: .+/u;
57
+ // Conventional Commit subject: squash merges promote the PR title to the
58
+ // commit subject on main, and no CI gate lints it, so this validation is
59
+ // the enforcement point for poller-authored PRs.
60
+ const PR_TITLE_PATTERN =
61
+ /^(?:build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(?:\([a-z0-9./-]+\))?!?: [^ \n\r\u2028\u2029]/u;
60
62
 
61
63
  export const readFixOutcome = async (
62
64
  workDir: string,
@@ -7,7 +7,7 @@ import {
7
7
  rootScriptExpectations,
8
8
  type StructureProfile,
9
9
  } from './structure-profile';
10
- import { hasSafeCommands, isSafeFilteredTurboAlias } from './structure-script';
10
+ import { filteredTurboAliasProblem, hasSafeCommands } from './structure-script';
11
11
  import { inspectWorkspace, type Workspace } from './structure-workspace';
12
12
 
13
13
  const ROOT_FIXED_SCRIPTS = new Set(
@@ -145,15 +145,13 @@ const inspectRootScripts = (
145
145
  `package.json: root script "${name}" must run ${exact ? 'exactly ' : ''}${requirement}`,
146
146
  ];
147
147
  });
148
- const aliasProblems = Object.entries(scripts).flatMap(([name, script]) =>
149
- ROOT_FIXED_SCRIPTS.has(name) ||
150
- typeof script !== 'string' ||
151
- isSafeFilteredTurboAlias(script)
152
- ? []
153
- : [
154
- `package.json: root script "${name}" must delegate through Turbo with an explicit --filter`,
155
- ],
156
- );
148
+ const aliasProblems = Object.entries(scripts).flatMap(([name, script]) => {
149
+ if (ROOT_FIXED_SCRIPTS.has(name) || typeof script !== 'string') {
150
+ return [];
151
+ }
152
+ const problem = filteredTurboAliasProblem(name, script);
153
+ return problem === null ? [] : [problem];
154
+ });
157
155
  return [...gateProblems, ...aliasProblems];
158
156
  };
159
157
  export const collectStructureProblems = async (
@@ -2,6 +2,18 @@ const UNSAFE_SCRIPT_SYNTAX = /[|;#"'`\r\n]/u;
2
2
  const SCRIPT_WHITESPACE = /\s+/u;
3
3
  const NON_EXECUTING_TURBO_OPTION = /^(?:-h|-v|--(?:dry|help|version))(?:=|$)/u;
4
4
 
5
+ type FilteredTurboAliasViolation =
6
+ | 'empty'
7
+ | 'filter-empty'
8
+ | 'filter-missing'
9
+ | 'filter-option'
10
+ | 'multiple-commands'
11
+ | 'non-executing-option'
12
+ | 'not-run'
13
+ | 'not-turbo'
14
+ | 'syntax'
15
+ | 'task';
16
+
5
17
  export const parseSafeCommands = (
6
18
  script: string | null,
7
19
  ): ReadonlyArray<ReadonlyArray<string>> | null => {
@@ -39,12 +51,32 @@ export const hasSafeCommand = (
39
51
  expected: string,
40
52
  ): boolean => hasSafeCommands(script, [expected], false);
41
53
 
42
- export const isSafeFilteredTurboAlias = (script: string): boolean => {
54
+ const filteredTurboAliasViolation = (
55
+ script: string,
56
+ ): FilteredTurboAliasViolation | null => {
57
+ if (script.trim() === '') {
58
+ return 'empty';
59
+ }
43
60
  const commands = parseSafeCommands(script);
44
- if (commands?.length !== 1) {
45
- return false;
61
+ if (commands === null) {
62
+ return 'syntax';
63
+ }
64
+ if (commands.length !== 1) {
65
+ return 'multiple-commands';
46
66
  }
47
67
  const [tokens] = commands;
68
+ if (tokens[0] !== 'turbo') {
69
+ return 'not-turbo';
70
+ }
71
+ if (tokens[1] !== 'run') {
72
+ return 'not-run';
73
+ }
74
+ if (tokens.some((token) => NON_EXECUTING_TURBO_OPTION.test(token))) {
75
+ return 'non-executing-option';
76
+ }
77
+ if (tokens[2]?.startsWith('-') !== false) {
78
+ return 'task';
79
+ }
48
80
  const filterAt = tokens.findIndex(
49
81
  (token) => token === '--filter' || token.startsWith('--filter='),
50
82
  );
@@ -53,11 +85,40 @@ export const isSafeFilteredTurboAlias = (script: string): boolean => {
53
85
  filter === '--filter'
54
86
  ? tokens[filterAt + 1]
55
87
  : filter?.slice('--filter='.length);
56
- return tokens[0] !== 'turbo' || tokens[1] !== 'run'
57
- ? false
58
- : tokens[2]?.startsWith('-') === false &&
59
- !tokens.some((token) => NON_EXECUTING_TURBO_OPTION.test(token)) &&
60
- filterValue !== undefined &&
61
- filterValue !== '' &&
62
- !filterValue.startsWith('-');
88
+ if (filter === undefined) {
89
+ return 'filter-missing';
90
+ }
91
+ if (filterValue === undefined || filterValue === '') {
92
+ return 'filter-empty';
93
+ }
94
+ return filterValue.startsWith('-') ? 'filter-option' : null;
63
95
  };
96
+
97
+ export const filteredTurboAliasProblem = (
98
+ name: string,
99
+ script: string,
100
+ ): string | null => {
101
+ const violation = filteredTurboAliasViolation(script);
102
+ if (violation === null) {
103
+ return null;
104
+ }
105
+ const details: Record<FilteredTurboAliasViolation, string> = {
106
+ empty: 'must be a non-empty single filtered Turbo command',
107
+ 'filter-empty': 'must pass a non-empty value to --filter',
108
+ 'filter-missing': 'must pass an explicit --filter',
109
+ 'filter-option': 'must pass a --filter value that is not another option',
110
+ 'multiple-commands':
111
+ 'must contain exactly one command; && command chains are not supported',
112
+ 'non-executing-option':
113
+ 'must execute a task; Turbo help, version, and dry-run options are not allowed',
114
+ 'not-run': 'must invoke Turbo through "turbo run"',
115
+ 'not-turbo': 'must invoke Turbo directly with "turbo"',
116
+ syntax:
117
+ 'contains shell syntax the structure gate does not parse (quotes, |, ;, #, backticks, $(, CR/LF line breaks, or malformed ampersand separators); write one command with plain arguments, using --filter=./apps/* for a glob',
118
+ task: 'must put a task name immediately after "turbo run"',
119
+ };
120
+ return `package.json: root script "${name}" ${details[violation]}`;
121
+ };
122
+
123
+ export const isSafeFilteredTurboAlias = (script: string): boolean =>
124
+ filteredTurboAliasViolation(script) === null;