From 1b470913c926e48ce66da8e9e85208bbcc4c3743 Mon Sep 17 00:00:00 2001 From: Conal <33135619+Conalh@users.noreply.github.com> Date: Thu, 21 May 2026 18:40:54 -0700 Subject: [PATCH 1/3] Migrate JSONC reader + line locators to agent-gov-core@v0.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces local hand-rolled JSON.parse and regex line locators with the shared primitives. Side-effect: JSONC comments and trailing commas are now stripped before parsing — the previous implementation would have thrown on Claude settings with `//` comments. The async ENOENT wrapper stays here because that's ScopeTrail-specific behavior (missing settings = empty config, not a parse error). All 37 existing tests pass against the migrated reader. Co-Authored-By: Claude Opus 4.7 --- dist/discovery.js | 25 ++++++++++++------------- package-lock.json | 11 +++++++++++ package.json | 3 +++ src/discovery.ts | 31 ++++++++++++++++--------------- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/dist/discovery.js b/dist/discovery.js index e955155..09c62a3 100644 --- a/dist/discovery.js +++ b/dist/discovery.js @@ -1,12 +1,19 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { stripJsonComments, lineOfJsonKey as coreLineOfJsonKey, lineOfJsonStringValue as coreLineOfJsonStringValue, } from 'agent-gov-core'; export async function readJsonObject(path) { return (await readJsonObjectWithSource(path)).json; } +/** + * Read a JSONC file. Comments and trailing commas are stripped via + * agent-gov-core, then JSON.parse runs against the stripped (but + * position-preserving) text. Missing files resolve to an empty object so + * detectors can run on repos that haven't adopted Claude settings yet. + */ export async function readJsonObjectWithSource(path) { try { const raw = await readFile(path, 'utf8'); - const parsed = JSON.parse(raw); + const parsed = JSON.parse(stripJsonComments(raw)); return { json: isRecord(parsed) ? parsed : {}, text: raw }; } catch (error) { @@ -23,20 +30,12 @@ export function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } export function lineOfJsonKey(text, key) { - const keyPattern = new RegExp(`"${escapeRegExp(key)}"\\s*:`); - return lineOfPattern(text, keyPattern); + const line = coreLineOfJsonKey(text, key); + return line === 0 ? undefined : line; } export function lineOfJsonStringValue(text, value) { - const encoded = JSON.stringify(value); - return lineOfPattern(text, new RegExp(escapeRegExp(encoded))); -} -function lineOfPattern(text, pattern) { - const lines = text.split(/\r?\n/); - const index = lines.findIndex((line) => pattern.test(line)); - return index === -1 ? undefined : index + 1; -} -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const line = coreLineOfJsonStringValue(text, value); + return line === 0 ? undefined : line; } function isNodeError(error) { return error instanceof Error && 'code' in error; diff --git a/package-lock.json b/package-lock.json index fbc3123..1ce7795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "scopetrail", "version": "0.1.10", "license": "MIT", + "dependencies": { + "agent-gov-core": "github:Conalh/agent-gov-core#v0.1.1" + }, "bin": { "scopetrail": "dist/index.js" }, @@ -26,6 +29,14 @@ "undici-types": "~7.16.0" } }, + "node_modules/agent-gov-core": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/Conalh/agent-gov-core.git#503b30f5aebf2eb0ebe6f4a60cd3cde14068670b", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index 4c7d283..3f6798b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "build": "tsc -p tsconfig.json", "test": "node --test" }, + "dependencies": { + "agent-gov-core": "github:Conalh/agent-gov-core#v0.1.1" + }, "devDependencies": { "@types/node": "^24.0.0", "typescript": "^5.9.3" diff --git a/src/discovery.ts b/src/discovery.ts index ebe6114..691d9ee 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -1,5 +1,10 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { + stripJsonComments, + lineOfJsonKey as coreLineOfJsonKey, + lineOfJsonStringValue as coreLineOfJsonStringValue, +} from 'agent-gov-core'; export async function readJsonObject(path: string): Promise> { return (await readJsonObjectWithSource(path)).json; @@ -10,10 +15,16 @@ export interface JsonObjectSource { text: string; } +/** + * Read a JSONC file. Comments and trailing commas are stripped via + * agent-gov-core, then JSON.parse runs against the stripped (but + * position-preserving) text. Missing files resolve to an empty object so + * detectors can run on repos that haven't adopted Claude settings yet. + */ export async function readJsonObjectWithSource(path: string): Promise { try { const raw = await readFile(path, 'utf8'); - const parsed: unknown = JSON.parse(raw); + const parsed: unknown = JSON.parse(stripJsonComments(raw)); return { json: isRecord(parsed) ? parsed : {}, text: raw }; } catch (error) { if (isNodeError(error) && error.code === 'ENOENT') { @@ -33,23 +44,13 @@ export function isRecord(value: unknown): value is Record { } export function lineOfJsonKey(text: string, key: string): number | undefined { - const keyPattern = new RegExp(`"${escapeRegExp(key)}"\\s*:`); - return lineOfPattern(text, keyPattern); + const line = coreLineOfJsonKey(text, key); + return line === 0 ? undefined : line; } export function lineOfJsonStringValue(text: string, value: string): number | undefined { - const encoded = JSON.stringify(value); - return lineOfPattern(text, new RegExp(escapeRegExp(encoded))); -} - -function lineOfPattern(text: string, pattern: RegExp): number | undefined { - const lines = text.split(/\r?\n/); - const index = lines.findIndex((line) => pattern.test(line)); - return index === -1 ? undefined : index + 1; -} - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const line = coreLineOfJsonStringValue(text, value); + return line === 0 ? undefined : line; } function isNodeError(error: unknown): error is NodeJS.ErrnoException { From 354510f9b19f0855673d8c075ad013d90d1c196b Mon Sep 17 00:00:00 2001 From: Conal <33135619+Conalh@users.noreply.github.com> Date: Thu, 21 May 2026 18:42:33 -0700 Subject: [PATCH 2/3] Install runtime deps in composite action before invoking dist Action used to run dist/index.js directly without npm install, which worked when ScopeTrail had no runtime deps. Now that discovery.ts imports agent-gov-core, the action needs to populate node_modules first. Co-Authored-By: Claude Opus 4.7 --- action.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/action.yml b/action.yml index 3177f6c..e599806 100644 --- a/action.yml +++ b/action.yml @@ -35,6 +35,10 @@ outputs: runs: using: composite steps: + - name: Install ScopeTrail dependencies + shell: bash + working-directory: ${{ github.action_path }} + run: npm ci --omit=dev --no-audit --no-fund - name: Run ScopeTrail permission drift review id: run shell: bash From 852c10d65adc111831188fec66cac85462aa9d47 Mon Sep 17 00:00:00 2001 From: Conal <33135619+Conalh@users.noreply.github.com> Date: Thu, 21 May 2026 18:43:56 -0700 Subject: [PATCH 3/3] Loosen action-metadata test to allow runtime deps install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-migration invariant 'no npm ci in action.yml' is replaced with 'no npm run build at action time' — dist/ is still committed and consumers still skip the TypeScript compile, but they do an --omit=dev install so runtime imports (now including agent-gov-core) resolve. Co-Authored-By: Claude Opus 4.7 --- test/action-metadata.test.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/action-metadata.test.mjs b/test/action-metadata.test.mjs index df7fd0b..80e56b5 100644 --- a/test/action-metadata.test.mjs +++ b/test/action-metadata.test.mjs @@ -24,14 +24,18 @@ test('GitHub Action metadata exposes PR drift inputs', async () => { assert.match(action, /--format github/); }); -test('GitHub Action uses committed runtime without installing dependencies in consumer workflows', async () => { +test('GitHub Action uses committed dist and a deps-only install (no build) in consumer workflows', async () => { const action = await readFile(join(packageRoot, 'action.yml'), 'utf8'); const gitignore = await readFile(join(packageRoot, '.gitignore'), 'utf8'); assert.match(action, /node "\$GITHUB_ACTION_PATH\/dist\/index\.js" diff --repo/); - assert.doesNotMatch(action, /npm ci/); + // dist/ is committed so consumers don't run a TypeScript build at action time. assert.doesNotMatch(action, /npm run build/); + assert.doesNotMatch(action, /tsc /); assert.doesNotMatch(gitignore, /^dist\/$/m); + // After the agent-gov-core migration the action installs runtime deps only + // (--omit=dev) so the external import resolves without a build step. + assert.match(action, /npm ci .*--omit=dev/); }); test('public Action install tags match package version', async () => {