Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ outputs:
runs:
using: composite
steps:
- name: Install PolicyMesh dependencies
shell: bash
working-directory: ${{ github.action_path }}
run: npm ci --omit=dev --no-audit --no-fund
- name: Run PolicyMesh agent policy audit
id: run
shell: bash
Expand Down
117 changes: 5 additions & 112 deletions dist/discovery.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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;
}
Expand Down Expand Up @@ -33,127 +34,19 @@ export async function readJsonObjectWithSource(path) {
throw error;
}
}
// VS Code and Cursor both ship MCP configs as JSONC — comments and the
// occasional trailing comma are normal, not malformed. We strip them
// before JSON.parse so those files audit cleanly. Replacing comment
// bytes with spaces (and preserving newlines in block comments) keeps
// the original byte/line positions intact for error reporting and the
// downstream line locators in lineOfJsonKey / lineOfJsonStringValue.
function stripJsonComments(raw) {
let out = '';
let inString = false;
let escape = false;
for (let index = 0; index < raw.length; index += 1) {
const char = raw[index];
const next = raw[index + 1];
if (inString) {
out += char;
if (escape) {
escape = false;
}
else if (char === '\\') {
escape = true;
}
else if (char === '"') {
inString = false;
}
continue;
}
if (char === '"') {
inString = true;
out += char;
continue;
}
if (char === '/' && next === '/') {
while (index < raw.length && raw[index] !== '\n') {
out += ' ';
index += 1;
}
// restore loop invariant: the for-loop's index++ will advance past '\n'
if (index < raw.length) {
out += raw[index];
}
continue;
}
if (char === '/' && next === '*') {
out += ' ';
index += 2;
while (index < raw.length && !(raw[index] === '*' && raw[index + 1] === '/')) {
out += raw[index] === '\n' ? '\n' : ' ';
index += 1;
}
if (index < raw.length) {
out += ' ';
index += 1; // for-loop will advance past the '/'
}
continue;
}
out += char;
}
return stripTrailingCommas(out);
}
// Trailing commas before `]` or `}` are legal in JSONC; JSON.parse rejects
// them. Removing them after comment-stripping keeps byte positions stable
// because we replace each removed comma with a space.
function stripTrailingCommas(raw) {
let out = '';
let inString = false;
let escape = false;
for (let index = 0; index < raw.length; index += 1) {
const char = raw[index];
if (inString) {
out += char;
if (escape) {
escape = false;
}
else if (char === '\\') {
escape = true;
}
else if (char === '"') {
inString = false;
}
continue;
}
if (char === '"') {
inString = true;
out += char;
continue;
}
if (char === ',') {
let look = index + 1;
while (look < raw.length && /\s/.test(raw[look])) {
look += 1;
}
if (raw[look] === ']' || raw[look] === '}') {
out += ' ';
continue;
}
}
out += char;
}
return out;
}
export function configPath(root, relativePath) {
return join(root, relativePath);
}
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 lineOfJsonParseError(text, error) {
const positionMatch = /position (\d+)/.exec(error.message);
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"build": "tsc -p tsconfig.json",
"test": "node --test"
},
"dependencies": {
"agent-gov-core": "github:Conalh/agent-gov-core#v0.1.1"

Check warning on line 34 in package.json

View workflow job for this annotation

GitHub Actions / scope-review

TaskBound medium scope creep

Added dependency agent-gov-core@github:Conalh/agent-gov-core#v0.1.1. Recommendation: Confirm the dependency is required for the stated task.
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "^5.9.3"
Expand Down
134 changes: 9 additions & 125 deletions src/discovery.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, unknown>> {
return (await readJsonObjectWithSource(path)).json;
Expand Down Expand Up @@ -49,117 +54,6 @@ export async function readJsonObjectWithSource(path: string): Promise<JsonObject
}
}

// VS Code and Cursor both ship MCP configs as JSONC — comments and the
// occasional trailing comma are normal, not malformed. We strip them
// before JSON.parse so those files audit cleanly. Replacing comment
// bytes with spaces (and preserving newlines in block comments) keeps
// the original byte/line positions intact for error reporting and the
// downstream line locators in lineOfJsonKey / lineOfJsonStringValue.
function stripJsonComments(raw: string): string {
let out = '';
let inString = false;
let escape = false;

for (let index = 0; index < raw.length; index += 1) {
const char = raw[index];
const next = raw[index + 1];

if (inString) {
out += char;
if (escape) {
escape = false;
} else if (char === '\\') {
escape = true;
} else if (char === '"') {
inString = false;
}
continue;
}

if (char === '"') {
inString = true;
out += char;
continue;
}

if (char === '/' && next === '/') {
while (index < raw.length && raw[index] !== '\n') {
out += ' ';
index += 1;
}
// restore loop invariant: the for-loop's index++ will advance past '\n'
if (index < raw.length) {
out += raw[index];
}
continue;
}

if (char === '/' && next === '*') {
out += ' ';
index += 2;
while (index < raw.length && !(raw[index] === '*' && raw[index + 1] === '/')) {
out += raw[index] === '\n' ? '\n' : ' ';
index += 1;
}
if (index < raw.length) {
out += ' ';
index += 1; // for-loop will advance past the '/'
}
continue;
}

out += char;
}

return stripTrailingCommas(out);
}

// Trailing commas before `]` or `}` are legal in JSONC; JSON.parse rejects
// them. Removing them after comment-stripping keeps byte positions stable
// because we replace each removed comma with a space.
function stripTrailingCommas(raw: string): string {
let out = '';
let inString = false;
let escape = false;

for (let index = 0; index < raw.length; index += 1) {
const char = raw[index];

if (inString) {
out += char;
if (escape) {
escape = false;
} else if (char === '\\') {
escape = true;
} else if (char === '"') {
inString = false;
}
continue;
}

if (char === '"') {
inString = true;
out += char;
continue;
}

if (char === ',') {
let look = index + 1;
while (look < raw.length && /\s/.test(raw[look])) {
look += 1;
}
if (raw[look] === ']' || raw[look] === '}') {
out += ' ';
continue;
}
}

out += char;
}

return out;
}

export function configPath(root: string, relativePath: string): string {
return join(root, relativePath);
}
Expand All @@ -169,23 +63,13 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
}

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 lineOfJsonParseError(text: string, error: SyntaxError): number | undefined {
Expand Down
2 changes: 1 addition & 1 deletion test/workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ test('published Action runs the bundled CLI without installing or rebuilding its
const trackedDistFiles = stdout.trim().split(/\r?\n/).filter(Boolean);

assert.match(action, /node "\$GITHUB_ACTION_PATH\/dist\/index\.js" audit --repo/);
assert.doesNotMatch(action, /npm ci/);
assert.match(action, /npm ci .*--omit=dev/);
assert.doesNotMatch(action, /npm run build/);
assert.doesNotMatch(gitignore, /^dist\/\s*$/m);
assert.ok(trackedDistFiles.includes('dist/index.js'));
Expand Down