First public release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:49:02 -04:00
co-authored by Claude Opus 5.5
commit 8dfe18703b
17 changed files with 2385 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import type {
PolicyReport,
PolicyRule,
RuleResult,
SourceFile,
Violation,
} from './types.js'
function matchesPath(path: string, includes: string[]): boolean {
return includes.some(i => path.includes(i))
}
/**
* A regex with the `g` flag carries `lastIndex` between `.test()` calls, so the
* same pattern reused across files silently starts matching from wherever the
* previous file left off — producing false passes that look like clean runs.
* Test against a fresh, non-global copy every time.
*/
function test(re: RegExp, content: string): boolean {
return new RegExp(re.source, re.flags.replace(/[gy]/g, '')).test(content)
}
/**
* Locate the trigger match so a finding can cite a line, not just a file.
*
* Scans line by line rather than counting newlines before a whole-file match
* index, because the trigger may legitimately span nothing more than one line
* and this keeps the excerpt exactly what the reader will see in the editor.
*/
function locate(re: RegExp, content: string): { line: number, excerpt: string } | undefined {
const bare = new RegExp(re.source, re.flags.replace(/[gy]/g, ''))
const lines = content.split('\n')
for (let i = 0; i < lines.length; i++) {
if (bare.test(lines[i]!)) return { line: i + 1, excerpt: lines[i]!.trim().slice(0, 120) }
}
// The trigger matched the file but not any single line — a multi-line
// pattern. Cite the file without a line rather than inventing one.
return undefined
}
/** Evaluate one rule over already-loaded files. Pure — no I/O, no git. */
export function evaluateRule(rule: PolicyRule, files: SourceFile[]): RuleResult {
let considered = 0
let subject = 0
let exempted = 0
const violations: Violation[] = []
const compliantPaths: string[] = []
const exemptedPaths: string[] = []
for (const f of files) {
if (!matchesPath(f.path, rule.pathIncludes)) continue
considered++
if (!test(rule.trigger, f.content)) continue
subject++
if (rule.allow.some(a => test(a, f.content))) {
compliantPaths.push(f.path)
continue
}
if (f.content.includes(rule.exemptMarker)) {
exempted++
exemptedPaths.push(f.path)
continue
}
const at = locate(rule.trigger, f.content)
violations.push({
ruleId: rule.id,
path: f.path,
reason: rule.description,
line: at?.line,
excerpt: at?.excerpt,
})
}
return { rule, considered, subject, exempted, violations, compliantPaths, exemptedPaths }
}
/**
* Evaluate every rule.
*
* `vacuous` is reported separately from `passed`. A run where no changed file
* was subject to any rule proves nothing — treating it as a pass is how a gate
* ends up green for months while covering nothing, which is the same defect as
* a test that cannot fail.
*/
export function evaluatePolicy(rules: PolicyRule[], files: SourceFile[]): PolicyReport {
const results = rules.map(r => evaluateRule(r, files))
const violations = results.flatMap(r => r.violations)
return {
results,
violations,
passed: violations.length === 0,
vacuous: results.every(r => r.subject === 0),
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from './types.js'
export * from './evaluate.js'
export * from './rules.js'
export * from './report.js'
+90
View File
@@ -0,0 +1,90 @@
import {
type CapabilityReport,
type Finding,
evidence,
formatReport as renderReport,
type FormatOptions,
} from '@extant2000/evidence-record'
import type { PolicyReport, RuleResult } from './types.js'
/**
* One finding per rule, in the shared vocabulary.
*
* The three-way split is the point of this library and now has three-way
* types to carry it:
*
* nothing was subject to the rule -> not-assessed (used to print VACUOUS)
* files were subject and complied -> pass, citing the files it checked
* files were subject and did not -> fail, citing file:line and the line
*
* "Nothing changed that this rule covers" and "the policy holds" are different
* statements, and a gate that prints ✓ for both is indistinguishable from one
* that does nothing.
*/
function findingFor(r: RuleResult): Finding {
const doc = evidence.document(r.rule.description, r.rule.id)
if (r.violations.length > 0) {
return {
id: r.rule.id,
summary: `${r.rule.id} — ${r.violations.length} file(s) subject to the rule did not satisfy it`,
determination: 'fail',
severity: 'high',
detail: r.rule.remedy.join(' '),
evidence: [
...r.violations.map(v => evidence.file(v.path, v.line, v.excerpt, v.reason)),
doc,
],
}
}
if (r.subject === 0) {
return {
id: r.rule.id,
summary: `${r.rule.id} — ${r.considered} file(s) examined, none subject to the rule`,
determination: 'not-assessed',
severity: 'medium',
detail: 'Nothing was checked against this rule. That is not evidence the policy holds.',
// Deliberately empty: the whole content of this finding is that there
// was nothing to observe.
evidence: [],
}
}
const ex = r.exempted ? `, ${r.exempted} exempted` : ''
return {
id: r.rule.id,
summary: `${r.rule.id} — ${r.subject} file(s) checked and compliant${ex}`,
determination: 'pass',
severity: 'info',
evidence: [
...r.compliantPaths.map(p => evidence.file(p, undefined, undefined, 'satisfied the rule')),
// Exemptions are evidence too, and printing them is what keeps an
// exemption from being a quiet way to pass.
...r.exemptedPaths.map(p => evidence.file(p, undefined, undefined, `exempted via "${r.rule.exemptMarker}"`)),
doc,
],
}
}
/** Convert a policy run into the portfolio-standard report shape. */
export function toReport(report: PolicyReport): CapabilityReport {
const considered = report.results.reduce((n, r) => n + r.considered, 0)
return {
capability: 'PolicyEnforced',
scope: `${report.results.length} rule(s) over ${considered} file(s)`,
examined: considered,
findings: report.results.map(findingFor),
}
}
/**
* Render a report for a CI log.
*
* Delegates to the shared renderer. The distinction that used to be carried
* by the word VACUOUS is now carried by NOT ASSESSED plus a sentence, which
* means the same thing without requiring a glossary.
*/
export function formatReport(report: PolicyReport, opts: FormatOptions = {}): string {
return renderReport(toReport(report), opts)
}
+110
View File
@@ -0,0 +1,110 @@
import type { PolicyRule } from './types.js'
/**
* Rule factories for three common gates.
*
* Each takes the parts that vary per app. An org-scoping gate differs between
* apps precisely because these lists differ (one app guards with
* `requireOrg` and tenant columns, another with RBAC capability checks), so
* they are parameters, not constants.
*/
export interface OrgScopingOptions {
/** Accessors that BYPASS row-level security. */
serviceClients?: RegExp
/** Named guards that make a service-role handler safe. */
guards: RegExp
/** Optional: inline tenant-column filter that also counts as scoped. */
inlineTenantFilter?: RegExp
pathIncludes?: string[]
}
/**
* A changed handler using a service-role client (RLS bypassed) must carry an
* authorization check or a documented exemption.
*
* Diff-scoped on purpose: it does not police pre-existing unguarded endpoints
* (track those separately), only new or changed code where the author is right there.
* A gate nobody trusts is worse than no gate.
*
* LIMITATION, inherited and worth restating: this confirms a guard is PRESENT,
* not that it filters the caller's own rows. Value-correctness is for tests and
* review.
*/
export function orgScopingRule(opts: OrgScopingOptions): PolicyRule {
const allow = [opts.guards]
if (opts.inlineTenantFilter) allow.push(opts.inlineTenantFilter)
return {
id: 'org-scoping',
description: 'service-role handler with no authorization check or exemption',
pathIncludes: opts.pathIncludes ?? ['server/api/'],
trigger: opts.serviceClients
?? /useServerSupabase|serverSupabaseServiceRole|createServiceClient|createClient\(/,
allow,
exemptMarker: 'org-scoping-exempt:',
remedy: [
'Add an authorization guard, or scope the query by a tenant column.',
'If the route is genuinely cross-tenant (cron, token, webhook, admin),',
'document it: // org-scoping-exempt: <reason>',
],
}
}
/**
* A changed file adding a dynamic `.in()` filter must chunk it.
*
* The bug: supabase-js puts every id in the query string, so past roughly 400
* UUIDs the request exceeds the header limit and dies — returning null, which
* `data || []` turns into a convincing empty state on top of real rows. It does
* not error. That is why this is a gate and not a code review note.
*/
export function inOverflowRule(opts: { chunkHelpers: RegExp, pathIncludes?: string[] }): PolicyRule {
return {
id: 'in-overflow',
description: 'dynamic .in() filter with no chunking helper or exemption',
pathIncludes: opts.pathIncludes ?? ['server/', 'app/'],
// `.in('col', someVariable)` — a literal array is bounded and fine.
trigger: /\.in\(\s*['"][^'"]+['"]\s*,\s*(?!\[)[A-Za-z_$]/,
allow: [opts.chunkHelpers],
exemptMarker: 'in-overflow-ok:',
remedy: [
'Chunk the filter (selectInChunks or equivalent), or document why the',
'list is bounded: // in-overflow-ok: <reason>',
],
}
}
/** A rule asserting a stated claim has an enforcement mechanism behind it. */
export interface ClaimOptions {
id: string
/** Where the claim is made (marketing copy, docs, policy pages). */
claimPaths: string[]
/** The claim text. */
claim: RegExp
/** Evidence that it is actually enforced. */
enforcedBy: RegExp
remedy?: string[]
}
/**
* The general "advertised == implemented" shape.
*
* The typical case: uptime or SLA language appears on pricing and marketing
* pages with no server-side enforcement anywhere. A public claim with nothing
* behind it is a compliance risk, not just a documentation gap.
*/
export function claimRule(opts: ClaimOptions): PolicyRule {
return {
id: opts.id,
description: 'public claim with no enforcement behind it',
pathIncludes: opts.claimPaths,
trigger: opts.claim,
allow: [opts.enforcedBy],
exemptMarker: `${opts.id}-exempt:`,
remedy: opts.remedy ?? [
'Either implement the enforcement, or remove/soften the claim.',
`If enforcement lives elsewhere, document it: // ${opts.id}-exempt: <where>`,
],
}
}
+103
View File
@@ -0,0 +1,103 @@
/**
* A policy that must hold in the running codebase, not merely in a document.
*
* Policy gates written as shell scripts tend to share one engine and differ
* only in their patterns. An org-scoping gate copied across several apps can
* legitimately end up with a distinct copy per app, and that divergence is
* CORRECT, not drift: one app scopes by an `org_id` tenant column, another has
* no tenant columns at all and authorises via RBAC capability checks. Forcing
* one script would break one of them.
*
* So the engine is shared and the policy is data.
*/
export interface PolicyRule {
/** Stable id, used in output and to reference a rule. */
id: string
/** One line: what must be true. */
description: string
/**
* Only files whose path matches one of these substrings/patterns are
* considered. Kept deliberately simple — a rule that needs a real glob
* engine is usually a rule that wants narrowing instead.
*/
pathIncludes: string[]
/**
* The file is SUBJECT to this rule only if its content matches. This is the
* risky construct — a service-role client, an unbounded `.in()`, and so on.
*/
trigger: RegExp
/**
* Any match here makes the file compliant. Multiple entries are OR-ed,
* because there is usually more than one legitimate way to satisfy a policy.
*/
allow: RegExp[]
/**
* A comment marker that documents a deliberate exemption, e.g.
* `org-scoping-exempt:`. Exemptions are part of a workable gate: without one,
* the legitimate cross-tenant cron job forces someone to disable the check
* entirely.
*/
exemptMarker: string
/** Lines printed under a violation telling the author how to fix it. */
remedy: string[]
}
export interface SourceFile {
path: string
content: string
}
export interface Violation {
ruleId: string
path: string
/** Why it failed, in one line. */
reason: string
/**
* 1-indexed line where the trigger matched — the construct that made this
* file subject to the rule. A violation that names only the file sends the
* reader hunting; this points at the line that did it.
*/
line?: number
/** The matched line, trimmed. */
excerpt?: string
}
export interface RuleResult {
rule: PolicyRule
/** Files the rule actually examined (matched pathIncludes). */
considered: number
/** Files that were subject to it (matched the trigger). */
subject: number
/** Files excused by an exemption marker. */
exempted: number
violations: Violation[]
/**
* Paths that were subject to the rule and satisfied it.
*
* Retained so a PASS can cite what it checked. A gate that reports "✓ 3
* files checked" without naming them is an asserted result, and this
* library's whole premise is that written policy and enforced policy are
* different things.
*/
compliantPaths: string[]
/** Paths excused by the exemption marker — counted, never hidden. */
exemptedPaths: string[]
}
export interface PolicyReport {
results: RuleResult[]
violations: Violation[]
passed: boolean
/**
* True when there was nothing to check. Distinguished from `passed` on
* purpose: "no changed files matched" is not evidence the policy holds, and
* a report that conflates them is the same lie as a test that cannot fail.
*/
vacuous: boolean
}