@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 +1 -1
- package/package.json +1 -1
- package/src/cli.ts +2 -2
- package/src/github-live-drift.ts +1 -1
- package/src/poller-outcome.ts +5 -3
- package/src/structure-check.ts +8 -10
- package/src/structure-script.ts +71 -10
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
|
|
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
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
|
|
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
|
|
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
|
}
|
package/src/github-live-drift.ts
CHANGED
|
@@ -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
|
|
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',
|
package/src/poller-outcome.ts
CHANGED
|
@@ -54,9 +54,11 @@ const REVIEW_STATUSES: ReadonlySet<string> = new Set([
|
|
|
54
54
|
'cannot-review',
|
|
55
55
|
]);
|
|
56
56
|
|
|
57
|
-
// Conventional Commit subject:
|
|
58
|
-
//
|
|
59
|
-
|
|
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,
|
package/src/structure-check.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
rootScriptExpectations,
|
|
8
8
|
type StructureProfile,
|
|
9
9
|
} from './structure-profile';
|
|
10
|
-
import {
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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 (
|
package/src/structure-script.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
45
|
-
return
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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;
|