@davidvornholt/standards 0.26.1 → 0.26.2

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
@@ -97,7 +97,7 @@ bun standards structure --profile source
97
97
 
98
98
  `doctor` checks project-owned integration points such as `biome.jsonc`, `AGENTS.local.md`, package scripts, sync policy, settings overlays, and Dependabot inputs.
99
99
 
100
- `structure` checks the exact Bun declaration, workspace scripts, internal dependency versions, package exports, TypeScript inheritance, accessibility wiring, workspace READMEs, and CI secret structure. The `source` profile records the few intentional differences in this repository itself.
100
+ `structure` checks the exact Bun declaration, workspace scripts, internal dependency versions, package exports, TypeScript inheritance, accessibility wiring, and CI secret structure. The `source` profile records the few intentional differences in this repository itself.
101
101
 
102
102
  ## dependabot
103
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.26.1",
3
+ "version": "0.26.2",
4
4
  "description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -166,7 +166,6 @@
166
166
  "src/structure-check.ts",
167
167
  "src/structure-bun-version.ts",
168
168
  "src/structure-profile.ts",
169
- "src/structure-readme.ts",
170
169
  "src/structure-secret-document.ts",
171
170
  "src/structure-script.ts",
172
171
  "src/structure-secrets.ts",
package/src/cli.ts CHANGED
@@ -1189,7 +1189,7 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
1189
1189
 
1190
1190
  // Canonical monorepo structure gate: workspace and root script shapes,
1191
1191
  // internal versioning, `exports`, tsconfig inheritance, a11y wiring,
1192
- // repo-owned workspace READMEs, and the undecrypted CI secrets contract
1192
+ // and the undecrypted CI secrets contract
1193
1193
  // (SOPS shape mirroring plus the keys the synced workflows require).
1194
1194
  // The `source` profile instead pins the standards template repository's own
1195
1195
  // deliberate exceptions to the consumer contract.
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import { isAbsolute, join } from 'node:path';
4
+ import { isContainedPath } from './contained-path';
4
5
  import { isRecord } from './github-settings-parse';
5
6
  import { readJsonFile } from './json-file';
6
7
  import { collectBunVersionProblems } from './structure-bun-version';
@@ -9,7 +10,6 @@ import {
9
10
  rootScriptExpectations,
10
11
  type StructureProfile,
11
12
  } from './structure-profile';
12
- import { collectWorkspaceReadmeProblems } from './structure-readme';
13
13
  import { filteredTurboAliasProblem, hasSafeCommands } from './structure-script';
14
14
  import { collectCiSecretsProblems } from './structure-secrets';
15
15
  import { inspectWorkspace, type Workspace } from './structure-workspace';
@@ -106,6 +106,12 @@ const loadWorkspace = async (
106
106
  if (!existsSync(path)) {
107
107
  return { workspace: null, problem: null };
108
108
  }
109
+ if (!isContainedPath(consumer, `${rel}/package.json`, 'file')) {
110
+ return {
111
+ workspace: null,
112
+ problem: `${rel}: package.json must be a contained regular file; symlinked paths are not allowed`,
113
+ };
114
+ }
109
115
  const manifest = await readJsonFile(path);
110
116
  if (manifest === null) {
111
117
  return {
@@ -174,16 +180,9 @@ export const collectStructureProblems = async (
174
180
  .map((ws) => ws.manifest.name)
175
181
  .filter((name): name is string => typeof name === 'string'),
176
182
  );
177
- const [inspections, readmeProblems] = await Promise.all([
178
- Promise.all(
179
- workspaces.map((ws) => inspectWorkspace(ws, workspaceNames, profile)),
180
- ),
181
- collectWorkspaceReadmeProblems(
182
- consumer,
183
- profile,
184
- workspaces.map((ws) => ws.rel),
185
- ),
186
- ]);
183
+ const inspections = await Promise.all(
184
+ workspaces.map((ws) => inspectWorkspace(ws, workspaceNames, profile)),
185
+ );
187
186
  const requireA11y = inspections.some((i) => i.hasA11ySuite);
188
187
  return [
189
188
  ...declaration.problems,
@@ -193,7 +192,6 @@ export const collectStructureProblems = async (
193
192
  ...collectBunVersionProblems(root),
194
193
  ...inspectRootScripts(root, profile, requireA11y),
195
194
  ...inspections.flatMap((i) => [...i.problems]),
196
- ...readmeProblems,
197
195
  ...secretsProblems,
198
196
  ];
199
197
  };
@@ -1,85 +0,0 @@
1
- import { lstat, readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { isContainedPath } from './contained-path';
4
- import { readJsonFile } from './json-file';
5
- import type { StructureProfile } from './structure-profile';
6
-
7
- const SYNC_MANIFEST = 'sync-standards.json';
8
- const TRAILING_SLASHES = /\/+$/u;
9
- const MANIFEST_REQUIREMENT = `${SYNC_MANIFEST}: must contain a JSON object with a "paths" array of strings; the structure gate reads it to tell canonical workspaces from repo-owned ones`;
10
- const MANIFEST_CONTAINMENT = `${SYNC_MANIFEST}: must be a contained regular file; symlinked paths are not allowed`;
11
-
12
- type CanonicalRels = {
13
- readonly rels: ReadonlySet<string>;
14
- readonly problem: string | null;
15
- };
16
-
17
- // Canonical workspaces are synced from the standards template and cannot be
18
- // fixed in a consumer checkout, so the consumer profile exempts them from
19
- // repo-owned documentation rules. The source repository owns those workspaces,
20
- // so the source profile exempts nothing.
21
- const canonicalWorkspaceRels = async (
22
- consumer: string,
23
- profile: StructureProfile,
24
- ): Promise<CanonicalRels> => {
25
- if (profile === 'source') {
26
- return { rels: new Set(), problem: null };
27
- }
28
- const manifestPath = join(consumer, SYNC_MANIFEST);
29
- const manifestInfo = await lstat(manifestPath).catch(() => null);
30
- if (
31
- manifestInfo !== null &&
32
- !isContainedPath(consumer, SYNC_MANIFEST, 'file')
33
- ) {
34
- return { rels: new Set(), problem: MANIFEST_CONTAINMENT };
35
- }
36
- const manifest = await readJsonFile(manifestPath);
37
- const paths = manifest?.paths;
38
- if (
39
- !(
40
- Array.isArray(paths) &&
41
- paths.every((path): path is string => typeof path === 'string')
42
- )
43
- ) {
44
- return { rels: new Set(), problem: MANIFEST_REQUIREMENT };
45
- }
46
- return {
47
- rels: new Set(paths.map((path) => path.replace(TRAILING_SLASHES, ''))),
48
- problem: null,
49
- };
50
- };
51
-
52
- const readmeProblem = async (
53
- consumer: string,
54
- rel: string,
55
- ): Promise<string | null> => {
56
- const readmeRel = `${rel}/README.md`;
57
- const readmePath = join(consumer, readmeRel);
58
- const info = await lstat(readmePath).catch(() => null);
59
- if (info !== null && !isContainedPath(consumer, readmeRel, 'file')) {
60
- return `${rel}: README.md must be a contained regular file; symlinked paths are not allowed`;
61
- }
62
- const raw = await readFile(readmePath, 'utf8').catch(() => null);
63
- return raw !== null && raw.trim() !== ''
64
- ? null
65
- : `${rel}: repo-owned workspace must have a non-empty README.md`;
66
- };
67
-
68
- export const collectWorkspaceReadmeProblems = async (
69
- consumer: string,
70
- profile: StructureProfile,
71
- rels: ReadonlyArray<string>,
72
- ): Promise<ReadonlyArray<string>> => {
73
- const canonical = await canonicalWorkspaceRels(consumer, profile);
74
- if (canonical.problem !== null) {
75
- // Without the manifest the gate cannot tell canonical workspaces from
76
- // repo-owned ones, so it fails on the manifest instead of guessing.
77
- return [canonical.problem];
78
- }
79
- const problems = await Promise.all(
80
- rels
81
- .filter((rel) => !canonical.rels.has(rel))
82
- .map((rel) => readmeProblem(consumer, rel)),
83
- );
84
- return problems.filter((problem): problem is string => problem !== null);
85
- };