@davidvornholt/standards 0.26.3 → 0.26.4

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
@@ -265,3 +265,7 @@ The worker accepts only exact issue or draft-PR revisions approved by a user wit
265
265
  The [root README](../../README.md) gives the short overview. The [adoption guide](../../docs/adoption.md) and [sync guide](../../docs/sync-and-ownership.md) explain repository ownership and first use.
266
266
 
267
267
  The poller timers use monotonic intervals: after boot, acknowledgement starts after one minute and work starts after two minutes. Missed downtime intervals are not replayed; each tick reads the current queue. The service budget reserves thirty minutes for scanning and setup in addition to its configured agent budget, with at most twelve watched repositories per poller. Split larger lists across pollers with independent API budgets; unusually slow provider or clone operations can exhaust that overhead and require investigating the service timeout before retrying.
268
+
269
+ ## Source text validation
270
+
271
+ `bun standards source-text` checks tracked source, configuration, and documentation files as UTF-8 text, regardless of Git’s binary classification. `bun standards check` includes this gate. Raw Unicode control characters are rejected except tab and LF; write control characters in fixtures and composite keys using escaped representations such as `\0` or `\u0000`. Binary asset formats and untracked files are outside this gate. Missing tracked source files and source symlinks fail validation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.26.3",
3
+ "version": "0.26.4",
4
4
  "description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -162,6 +162,7 @@
162
162
  "src/screenshots-publish.ts",
163
163
  "src/screenshots-upload.ts",
164
164
  "src/sops-exec.ts",
165
+ "src/source-text.ts",
165
166
  "src/structure-check.ts",
166
167
  "src/structure-bun-version.ts",
167
168
  "src/structure-profile.ts",
package/src/cli.ts CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  } from './managed-path-ownership';
45
45
  import { runPollerCommand } from './poller-commands';
46
46
  import { runScreenshotsCommand } from './screenshots-commands';
47
+ import { collectSourceTextProblems } from './source-text';
47
48
  import { collectStructureProblems } from './structure-check';
48
49
  import type { StructureProfile } from './structure-profile';
49
50
  import { hasSafeCommand } from './structure-script';
@@ -92,6 +93,7 @@ type Command =
92
93
  | 'help'
93
94
  | 'init'
94
95
  | 'poller'
96
+ | 'source-text'
95
97
  | 'structure'
96
98
  | 'sync';
97
99
 
@@ -969,6 +971,7 @@ Commands:
969
971
  sync Mirror canonical files from upstream, regenerate the composed Dependabot config, and rewrite the lock
970
972
  check Verify canonical files, extension seams, monorepo structure, and GitHub settings
971
973
  doctor Validate extension seams only
974
+ source-text Reject raw control characters in tracked source text
972
975
  structure Validate monorepo structure rules only
973
976
  dependabot Verify (--check) or regenerate (--write) the composed .github/dependabot.yml
974
977
  dev-env Compose each workspace's generated .env.local from its three dev layers and authorized broker-owned S3 pair references
@@ -1002,6 +1005,7 @@ const commandFromArg = (arg: string): Command => {
1002
1005
  arg === 'help' ||
1003
1006
  arg === 'init' ||
1004
1007
  arg === 'poller' ||
1008
+ arg === 'source-text' ||
1005
1009
  arg === 'structure' ||
1006
1010
  arg === 'sync'
1007
1011
  ) {
@@ -1209,10 +1213,24 @@ const runStructure = async (
1209
1213
  return true;
1210
1214
  };
1211
1215
 
1216
+ const runSourceTextCheck = (consumer: string): boolean => {
1217
+ const problems = collectSourceTextProblems(consumer);
1218
+ if (problems.length > 0) {
1219
+ console.error(`standards source-text: ${problems.length} problem(s):`);
1220
+ console.error(problems.map((problem) => ` - ${problem}`).join('\n'));
1221
+ return false;
1222
+ }
1223
+ console.log(
1224
+ 'standards source-text: tracked source text contains no forbidden control characters',
1225
+ );
1226
+ return true;
1227
+ };
1228
+
1212
1229
  const runCheckCommand = async (consumer: string): Promise<boolean> => {
1213
1230
  const driftIsClean = await runCheck(consumer);
1214
1231
  const integrationIsValid = await runDoctor(consumer);
1215
1232
  const structureIsValid = await runStructure(consumer, 'consumer');
1233
+ const sourceTextIsValid = runSourceTextCheck(consumer);
1216
1234
  // The GitHub gate activates with the synced declaration and then fails
1217
1235
  // closed: once .github/settings.json exists, an unreachable API or an
1218
1236
  // unreadable origin is a failure, not a skip.
@@ -1220,7 +1238,11 @@ const runCheckCommand = async (consumer: string): Promise<boolean> => {
1220
1238
  ? await runGithubCheckGate(consumer)
1221
1239
  : true;
1222
1240
  return (
1223
- driftIsClean && integrationIsValid && structureIsValid && githubIsConverged
1241
+ driftIsClean &&
1242
+ integrationIsValid &&
1243
+ structureIsValid &&
1244
+ sourceTextIsValid &&
1245
+ githubIsConverged
1224
1246
  );
1225
1247
  };
1226
1248
 
@@ -1296,6 +1318,7 @@ const runGateCommand = (
1296
1318
  | 'dev-env'
1297
1319
  | 'doctor'
1298
1320
  | 'github'
1321
+ | 'source-text'
1299
1322
  | 'structure',
1300
1323
  consumer: string,
1301
1324
  apply: boolean,
@@ -1313,6 +1336,9 @@ const runGateCommand = (
1313
1336
  if (command === 'doctor') {
1314
1337
  return runDoctor(consumer);
1315
1338
  }
1339
+ if (command === 'source-text') {
1340
+ return Promise.resolve(runSourceTextCheck(consumer));
1341
+ }
1316
1342
  if (command === 'structure') {
1317
1343
  return runStructure(consumer, profile);
1318
1344
  }
@@ -0,0 +1,98 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { basename, extname, join } from 'node:path';
4
+ import { isContainedPath } from './contained-path';
5
+
6
+ const sourceExtensions = new Set(
7
+ '.ts,.tsx,.mts,.cts,.js,.jsx,.mjs,.cjs,.json,.jsonc,.yaml,.yml,.toml,.nix,.sh,.bash,.zsh,.rb,.py,.css,.scss,.html,.xml,.svg,.md,.mdx,.sql,.just'.split(
8
+ ',',
9
+ ),
10
+ );
11
+ const sourceFileNames = new Set([
12
+ 'Dockerfile',
13
+ 'Containerfile',
14
+ 'Makefile',
15
+ 'justfile',
16
+ '.envrc',
17
+ '.gitignore',
18
+ '.gitattributes',
19
+ '.dockerignore',
20
+ '.editorconfig',
21
+ '.npmrc',
22
+ ]);
23
+ const dotenvFileName = /^\.env(?:\..*)?$/u;
24
+ const forbiddenControl = /[^\P{Cc}\t\n]/u;
25
+ const gitOutputLimit = 16_777_216;
26
+ const hexadecimalRadix = 16;
27
+ const minimumCodePointWidth = 4;
28
+
29
+ export const inspectSourceText = (
30
+ path: string,
31
+ bytes: Uint8Array,
32
+ ): string | null => {
33
+ let text: string;
34
+ try {
35
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
36
+ } catch {
37
+ return `${JSON.stringify(path)}: source text must be valid UTF-8`;
38
+ }
39
+ const match = forbiddenControl.exec(text);
40
+ if (!match) {
41
+ return null;
42
+ }
43
+ const point = match[0]
44
+ .codePointAt(0)
45
+ ?.toString(hexadecimalRadix)
46
+ .toUpperCase()
47
+ .padStart(minimumCodePointWidth, '0');
48
+ const line = text.slice(0, match.index).split('\n').length;
49
+ return `${JSON.stringify(path)}:${line}: raw control character U+${point}; use an escaped representation (only tab and LF are allowed)`;
50
+ };
51
+
52
+ export const collectSourceTextProblems = (
53
+ root: string,
54
+ ): ReadonlyArray<string> => {
55
+ let tracked: string;
56
+ try {
57
+ tracked = execFileSync('git', ['ls-files', '--cached', '-z'], {
58
+ cwd: root,
59
+ encoding: 'utf8',
60
+ maxBuffer: gitOutputLimit,
61
+ stdio: ['ignore', 'pipe', 'pipe'],
62
+ });
63
+ } catch {
64
+ return [
65
+ 'Cannot enumerate tracked source files; run the gate from a readable Git working tree.',
66
+ ];
67
+ }
68
+ const problems: Array<string> = [];
69
+ const paths = new Set(
70
+ tracked
71
+ .split('\0')
72
+ .filter(
73
+ (path) =>
74
+ sourceExtensions.has(extname(path)) ||
75
+ sourceFileNames.has(basename(path)) ||
76
+ dotenvFileName.test(basename(path)),
77
+ ),
78
+ );
79
+ for (const path of paths) {
80
+ if (isContainedPath(root, path, 'file')) {
81
+ try {
82
+ const problem = inspectSourceText(path, readFileSync(join(root, path)));
83
+ if (problem !== null) {
84
+ problems.push(problem);
85
+ }
86
+ } catch {
87
+ problems.push(
88
+ `${JSON.stringify(path)}: cannot read tracked source text`,
89
+ );
90
+ }
91
+ } else {
92
+ problems.push(
93
+ `${JSON.stringify(path)}: tracked source must be a contained regular file`,
94
+ );
95
+ }
96
+ }
97
+ return problems;
98
+ };
@@ -31,6 +31,7 @@ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
31
31
  name: 'check',
32
32
  commands: [
33
33
  `${SOURCE_CLI} structure --profile source`,
34
+ `${SOURCE_CLI} source-text`,
34
35
  `${SOURCE_CLI} dependabot --check`,
35
36
  `${SOURCE_CLI} github --check`,
36
37
  'turbo run lint check-types test --output-logs=errors-only',
@@ -41,6 +42,7 @@ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
41
42
  name: 'check:fix',
42
43
  commands: [
43
44
  `${SOURCE_CLI} structure --profile source`,
45
+ `${SOURCE_CLI} source-text`,
44
46
  `${SOURCE_CLI} dependabot --write`,
45
47
  `${SOURCE_CLI} github --check`,
46
48
  'turbo run lint:fix check-types test --output-logs=errors-only',