From 56e0fb0c57d3289f9b518ca7d5b3615d526047a5 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Tue, 19 May 2026 16:45:33 -0700 Subject: [PATCH 1/7] Validate that GAP directories only contain allowed files Restrict files in GAP directories to *.md and metadata.yml to prevent files that could poison developer environments (e.g. .nvmrc) from being merged. Co-Authored-By: Claude Opus 4.7 --- scripts/validate-structure.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/scripts/validate-structure.js b/scripts/validate-structure.js index 6c0b634..85683f2 100755 --- a/scripts/validate-structure.js +++ b/scripts/validate-structure.js @@ -9,7 +9,7 @@ * find ./gaps -maxdepth 1 -type d -name 'GAP-*' | xargs -I{} node scripts/validate-structure.js {} */ -import { existsSync, readFileSync, statSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { basename, join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; @@ -114,6 +114,26 @@ function validateMetadata(dirPath, gapName) { } } +function validateAllowedFiles(dirPath, gapName) { + const entries = readdirSync(dirPath); + for (const entry of entries) { + const fullPath = join(dirPath, entry); + if (statSync(fullPath).isDirectory()) { + error( + gapName, + `Unexpected directory "${entry}" found. GAP directories may only contain *.md files and metadata.yml. If you believe this is in error, please ping @graphql/gaps-editors.`, + ); + } + if (entry === "metadata.yml" || entry.endsWith(".md")) { + continue; + } + error( + gapName, + `Unexpected file "${entry}" found. GAP directories may only contain *.md files and metadata.yml. If you believe this is in error, please ping @graphql/gaps-editors.`, + ); + } +} + function main() { const { positionals } = parseArgs({ allowPositionals: true, strict: true }); @@ -137,6 +157,9 @@ function main() { // Validate directory naming const gapName = validateDirectoryNaming(dirPath); + // Validate only allowed files are present + validateAllowedFiles(dirPath, gapName); + // Validate README.md exists validateReadmeExists(dirPath, gapName); From ec8a520a5b7d23bc9c7acc072d5050844f4c6a8e Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Tue, 2 Jun 2026 11:22:29 -0500 Subject: [PATCH 2/7] Allow dotfiles and metadata.json in GAP directories Address review feedback: skip dotfiles (e.g. .gitkeep) from the allowlist check, and permit metadata.json alongside metadata.yml. Co-Authored-By: Claude Opus 4.8 --- scripts/validate-structure.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/validate-structure.js b/scripts/validate-structure.js index 85683f2..f9ce506 100755 --- a/scripts/validate-structure.js +++ b/scripts/validate-structure.js @@ -121,15 +121,20 @@ function validateAllowedFiles(dirPath, gapName) { if (statSync(fullPath).isDirectory()) { error( gapName, - `Unexpected directory "${entry}" found. GAP directories may only contain *.md files and metadata.yml. If you believe this is in error, please ping @graphql/gaps-editors.`, + `Unexpected directory "${entry}" found. GAP directories may only contain *.md files, metadata.yml, and metadata.json. If you believe this is in error, please ping @graphql/gaps-editors.`, ); } - if (entry === "metadata.yml" || entry.endsWith(".md")) { + if ( + entry === "metadata.yml" || + entry === "metadata.json" || + entry.endsWith(".md") || + entry.startsWith(".") + ) { continue; } error( gapName, - `Unexpected file "${entry}" found. GAP directories may only contain *.md files and metadata.yml. If you believe this is in error, please ping @graphql/gaps-editors.`, + `Unexpected file "${entry}" found. GAP directories may only contain *.md files, metadata.yml, and metadata.json. If you believe this is in error, please ping @graphql/gaps-editors.`, ); } } From bc63d3c24e30804372e2fc2ddf84519c8f8017d5 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Tue, 2 Jun 2026 11:27:14 -0500 Subject: [PATCH 3/7] Skip dotfiles before all checks, not inside the allowlist Co-Authored-By: Claude Opus 4.8 --- scripts/validate-structure.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/validate-structure.js b/scripts/validate-structure.js index f9ce506..9a6c322 100755 --- a/scripts/validate-structure.js +++ b/scripts/validate-structure.js @@ -117,6 +117,9 @@ function validateMetadata(dirPath, gapName) { function validateAllowedFiles(dirPath, gapName) { const entries = readdirSync(dirPath); for (const entry of entries) { + if (entry.startsWith(".")) { + continue; + } const fullPath = join(dirPath, entry); if (statSync(fullPath).isDirectory()) { error( @@ -127,8 +130,7 @@ function validateAllowedFiles(dirPath, gapName) { if ( entry === "metadata.yml" || entry === "metadata.json" || - entry.endsWith(".md") || - entry.startsWith(".") + entry.endsWith(".md") ) { continue; } From 1c0503521d34598908683738a62c9b51c2a05c63 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Tue, 2 Jun 2026 11:28:08 -0500 Subject: [PATCH 4/7] Reject dotfiles: use !entry.startsWith(".") in allowlist Co-Authored-By: Claude Opus 4.8 --- scripts/validate-structure.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/validate-structure.js b/scripts/validate-structure.js index 9a6c322..9754e29 100755 --- a/scripts/validate-structure.js +++ b/scripts/validate-structure.js @@ -117,9 +117,6 @@ function validateMetadata(dirPath, gapName) { function validateAllowedFiles(dirPath, gapName) { const entries = readdirSync(dirPath); for (const entry of entries) { - if (entry.startsWith(".")) { - continue; - } const fullPath = join(dirPath, entry); if (statSync(fullPath).isDirectory()) { error( @@ -130,7 +127,8 @@ function validateAllowedFiles(dirPath, gapName) { if ( entry === "metadata.yml" || entry === "metadata.json" || - entry.endsWith(".md") + entry.endsWith(".md") || + !entry.startsWith(".") ) { continue; } From 9512f7be6c8e19d2b414c982ea61a13616bb5656 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Tue, 2 Jun 2026 11:28:39 -0500 Subject: [PATCH 5/7] =?UTF-8?q?Remove=20dotfile=20exception=20=E2=80=94=20?= =?UTF-8?q?only=20allow=20*.md,=20metadata.yml,=20metadata.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dotfiles like .nvmrc should still be rejected. Co-Authored-By: Claude Opus 4.8 --- scripts/validate-structure.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/validate-structure.js b/scripts/validate-structure.js index 9754e29..0bdbb47 100755 --- a/scripts/validate-structure.js +++ b/scripts/validate-structure.js @@ -127,8 +127,7 @@ function validateAllowedFiles(dirPath, gapName) { if ( entry === "metadata.yml" || entry === "metadata.json" || - entry.endsWith(".md") || - !entry.startsWith(".") + entry.endsWith(".md") ) { continue; } From daa16fe23e5dbdef39e42a38be9b06d2ea1bf963 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Tue, 2 Jun 2026 11:29:59 -0500 Subject: [PATCH 6/7] Explicitly reject dotfiles before the allowlist check Catches cases like .foo.md that would otherwise pass the .md check. Co-Authored-By: Claude Opus 4.8 --- scripts/validate-structure.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/validate-structure.js b/scripts/validate-structure.js index 0bdbb47..aa8e546 100755 --- a/scripts/validate-structure.js +++ b/scripts/validate-structure.js @@ -117,6 +117,12 @@ function validateMetadata(dirPath, gapName) { function validateAllowedFiles(dirPath, gapName) { const entries = readdirSync(dirPath); for (const entry of entries) { + if (entry.startsWith(".")) { + error( + gapName, + `Dotfiles are not allowed: "${entry}". If you believe this is in error, please ping @graphql/gaps-editors.`, + ); + } const fullPath = join(dirPath, entry); if (statSync(fullPath).isDirectory()) { error( From 2a6978206b132ef9d2abe4f4ffd647a9ea22c804 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Tue, 2 Jun 2026 13:17:08 -0500 Subject: [PATCH 7/7] Refactor file validation and add versions/ directory support Two plain functions, no abstractions. Validates YYYY-MM.md and YYYY-MM.yml in versions/, rejects dotfiles at both levels. Co-Authored-By: Claude Opus 4.8 --- scripts/validate-structure.js | 52 ++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/scripts/validate-structure.js b/scripts/validate-structure.js index aa8e546..fa81059 100755 --- a/scripts/validate-structure.js +++ b/scripts/validate-structure.js @@ -115,32 +115,46 @@ function validateMetadata(dirPath, gapName) { } function validateAllowedFiles(dirPath, gapName) { - const entries = readdirSync(dirPath); - for (const entry of entries) { + for (const entry of readdirSync(dirPath)) { if (entry.startsWith(".")) { - error( - gapName, - `Dotfiles are not allowed: "${entry}". If you believe this is in error, please ping @graphql/gaps-editors.`, - ); + error(gapName, `Dotfiles are not allowed: "${entry}".`); + continue; } + const fullPath = join(dirPath, entry); + if (statSync(fullPath).isDirectory()) { - error( - gapName, - `Unexpected directory "${entry}" found. GAP directories may only contain *.md files, metadata.yml, and metadata.json. If you believe this is in error, please ping @graphql/gaps-editors.`, - ); + if (entry === "versions") { + validateVersionsDir(fullPath, gapName); + } else { + error(gapName, `Unexpected directory "${entry}".`); + } + continue; } - if ( - entry === "metadata.yml" || - entry === "metadata.json" || - entry.endsWith(".md") - ) { + + if (entry === "metadata.yml" || entry === "metadata.json" || entry.endsWith(".md")) { continue; } - error( - gapName, - `Unexpected file "${entry}" found. GAP directories may only contain *.md files, metadata.yml, and metadata.json. If you believe this is in error, please ping @graphql/gaps-editors.`, - ); + + error(gapName, `Unexpected file "${entry}".`); + } +} + +function validateVersionsDir(dirPath, gapName) { + for (const entry of readdirSync(dirPath)) { + if (entry.startsWith(".")) { + error(gapName, `Dotfiles are not allowed in versions/: "${entry}".`); + continue; + } + + if (statSync(join(dirPath, entry)).isDirectory()) { + error(gapName, `Unexpected directory in versions/: "${entry}".`); + continue; + } + + if (!/^\d{4}-\d{2}\.(md|yml)$/.test(entry)) { + error(gapName, `Unexpected file in versions/: "${entry}". Only YYYY-MM.md and YYYY-MM.yml are allowed.`); + } } }