@mmnto/totem 1.103.0 → 1.104.0
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/dist/config-schema.d.ts +34 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +14 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestration-resolver.d.ts.map +1 -1
- package/dist/orchestration-resolver.js +5 -1
- package/dist/orchestration-resolver.js.map +1 -1
- package/dist/orchestration-resolver.test.js +2 -2
- package/dist/orchestration-resolver.test.js.map +1 -1
- package/dist/parity-detect.d.ts +86 -0
- package/dist/parity-detect.d.ts.map +1 -1
- package/dist/parity-detect.js +595 -0
- package/dist/parity-detect.js.map +1 -1
- package/dist/parity-detect.network-posture.test.d.ts +19 -0
- package/dist/parity-detect.network-posture.test.d.ts.map +1 -0
- package/dist/parity-detect.network-posture.test.js +534 -0
- package/dist/parity-detect.network-posture.test.js.map +1 -0
- package/dist/parity-manifest.d.ts +8 -0
- package/dist/parity-manifest.d.ts.map +1 -1
- package/dist/parity-manifest.js +11 -0
- package/dist/parity-manifest.js.map +1 -1
- package/dist/parity-manifest.test.js +19 -0
- package/dist/parity-manifest.test.js.map +1 -1
- package/package.json +1 -1
package/dist/parity-detect.js
CHANGED
|
@@ -2027,4 +2027,599 @@ function isDirectory(p) {
|
|
|
2027
2027
|
return false;
|
|
2028
2028
|
}
|
|
2029
2029
|
}
|
|
2030
|
+
/**
|
|
2031
|
+
* The pinned squash-merge body/title posture (row-1). GitHub encodes these as
|
|
2032
|
+
* enums; the ruled values are a BLANK squash body + a PR_TITLE squash title.
|
|
2033
|
+
*/
|
|
2034
|
+
const EXPECTED_SQUASH_MESSAGE = 'BLANK';
|
|
2035
|
+
const EXPECTED_SQUASH_TITLE = 'PR_TITLE';
|
|
2036
|
+
/** The symbolic ref-name includes that mark a ruleset as targeting the default branch. */
|
|
2037
|
+
const DEFAULT_BRANCH_INCLUDES = new Set(['~DEFAULT_BRANCH', '~ALL']);
|
|
2038
|
+
/**
|
|
2039
|
+
* The ruleset rule types that gate writes to a branch (the "push/PR-class" set,
|
|
2040
|
+
* row-3). Any active ruleset carrying one of these for the default branch must
|
|
2041
|
+
* itself be un-bypassable — a permissive/bypassable ruleset must not undercut
|
|
2042
|
+
* classic branch protection.
|
|
2043
|
+
*/
|
|
2044
|
+
const PUSH_PR_RULE_TYPES = new Set([
|
|
2045
|
+
'pull_request',
|
|
2046
|
+
'non_fast_forward',
|
|
2047
|
+
'deletion',
|
|
2048
|
+
'creation',
|
|
2049
|
+
'update',
|
|
2050
|
+
'required_linear_history',
|
|
2051
|
+
'required_signatures',
|
|
2052
|
+
'required_status_checks',
|
|
2053
|
+
'required_deployments',
|
|
2054
|
+
'merge_queue',
|
|
2055
|
+
]);
|
|
2056
|
+
// ── Boundary Zod schemas (untrusted fetched JSON; max-tolerance) ──
|
|
2057
|
+
/** Row-1 repo-settings posture fields. A 200 missing any of these is auth-class (`unknown`). */
|
|
2058
|
+
const RepoMergeSettingsSchema = z.object({
|
|
2059
|
+
allow_squash_merge: z.boolean(),
|
|
2060
|
+
allow_merge_commit: z.boolean(),
|
|
2061
|
+
allow_rebase_merge: z.boolean(),
|
|
2062
|
+
squash_merge_commit_message: z.string(),
|
|
2063
|
+
squash_merge_commit_title: z.string(),
|
|
2064
|
+
});
|
|
2065
|
+
/** One ruleset rule (`{ type, parameters }`) — parameters stay `unknown` until a per-type narrow. */
|
|
2066
|
+
const RulesetRuleSchema = z.object({
|
|
2067
|
+
type: z.string(),
|
|
2068
|
+
parameters: z.unknown().optional(),
|
|
2069
|
+
});
|
|
2070
|
+
/** One ruleset detail object (max-tolerance — every field optional so a slim payload still narrows). */
|
|
2071
|
+
const RulesetSchema = z.object({
|
|
2072
|
+
id: z.union([z.number(), z.string()]).optional(),
|
|
2073
|
+
name: z.string().optional(),
|
|
2074
|
+
enforcement: z.string().optional(),
|
|
2075
|
+
target: z.string().optional(),
|
|
2076
|
+
conditions: z
|
|
2077
|
+
.object({
|
|
2078
|
+
ref_name: z
|
|
2079
|
+
.object({
|
|
2080
|
+
include: z.array(z.string()).optional(),
|
|
2081
|
+
exclude: z.array(z.string()).optional(),
|
|
2082
|
+
})
|
|
2083
|
+
.optional(),
|
|
2084
|
+
})
|
|
2085
|
+
.optional(),
|
|
2086
|
+
bypass_actors: z.array(z.unknown()).optional(),
|
|
2087
|
+
rules: z.array(RulesetRuleSchema).optional(),
|
|
2088
|
+
});
|
|
2089
|
+
/** The rulesets surface payload is an array of ruleset details. */
|
|
2090
|
+
const RulesetsArraySchema = z.array(RulesetSchema);
|
|
2091
|
+
/** Parameters of a `required_status_checks` rule (row-2 union + strict-policy read). */
|
|
2092
|
+
const StatusCheckParamsSchema = z.object({
|
|
2093
|
+
required_status_checks: z.array(z.object({ context: z.string() })).optional(),
|
|
2094
|
+
strict_required_status_checks_policy: z.boolean().optional(),
|
|
2095
|
+
});
|
|
2096
|
+
/** A classic branch-protection `{ enabled }` toggle. */
|
|
2097
|
+
const ProtectionToggleSchema = z.object({ enabled: z.boolean() });
|
|
2098
|
+
/**
|
|
2099
|
+
* Row-3 classic branch-protection posture. The three toggles are ALWAYS present
|
|
2100
|
+
* in a full admin read, so their absence marks an under-privileged 200
|
|
2101
|
+
* (auth-class `unknown`). `required_pull_request_reviews` is legitimately absent
|
|
2102
|
+
* when PR review is not required — that absence is real drift, not auth-class.
|
|
2103
|
+
*/
|
|
2104
|
+
const BranchProtectionSchema = z.object({
|
|
2105
|
+
required_pull_request_reviews: z
|
|
2106
|
+
.object({ required_approving_review_count: z.number().optional() })
|
|
2107
|
+
.optional(),
|
|
2108
|
+
enforce_admins: ProtectionToggleSchema,
|
|
2109
|
+
allow_force_pushes: ProtectionToggleSchema,
|
|
2110
|
+
allow_deletions: ProtectionToggleSchema,
|
|
2111
|
+
});
|
|
2112
|
+
/**
|
|
2113
|
+
* The totem-side canonical ruleset declaration (`.totem/rulesets/main.json`,
|
|
2114
|
+
* schema-version 1). The canonical required-checks list + pinned strict policy
|
|
2115
|
+
* come from `required_status_checks`; the surface is compared against THIS, never
|
|
2116
|
+
* against itself (Tenet 20).
|
|
2117
|
+
*/
|
|
2118
|
+
const RulesetDeclarationSchema = z.object({
|
|
2119
|
+
'schema-version': z.number(),
|
|
2120
|
+
'ruleset-name': z.string().optional(),
|
|
2121
|
+
enforcement: z.string().optional(),
|
|
2122
|
+
bypass_actors: z.array(z.unknown()).optional(),
|
|
2123
|
+
required_status_checks: z
|
|
2124
|
+
.object({
|
|
2125
|
+
strict_required_status_checks_policy: z.boolean().optional(),
|
|
2126
|
+
contexts: z.array(z.string()).optional(),
|
|
2127
|
+
})
|
|
2128
|
+
.optional(),
|
|
2129
|
+
});
|
|
2130
|
+
/**
|
|
2131
|
+
* Sense the three Prop 296 §14 network-read-only posture rows against pre-fetched
|
|
2132
|
+
* snapshots. Returns an ARRAY of per-repo verdict lines (the {@link LockContentLine}
|
|
2133
|
+
* pattern — the CLI's flatMap render + R2 contract-counting already support
|
|
2134
|
+
* multi-line rows). NEVER networks (the fetches ran at the CLI edge), NEVER
|
|
2135
|
+
* throws (every read/parse failure degrades to a verdict), NEVER emits `fail`
|
|
2136
|
+
* (the CLI edge owns `--strict` promotion) and NEVER a drift verdict on an
|
|
2137
|
+
* auth/transport failure (§14 clause 2).
|
|
2138
|
+
*
|
|
2139
|
+
* Applicability: the row's `consumers` scope is applied PER-REPO against each
|
|
2140
|
+
* snapshot's `repoId` (verbatim with {@link detectLockContentContract}). A row
|
|
2141
|
+
* scoped `consumers: [totem]` senses only the roster repos whose id is `totem`;
|
|
2142
|
+
* an empty in-scope set yields one honest-absent `skip`.
|
|
2143
|
+
*/
|
|
2144
|
+
export function detectNetworkPostureContract(contract, ctx) {
|
|
2145
|
+
const inScope = ctx.repos.filter((r) => repoInConsumerScope(contract, r.repoId));
|
|
2146
|
+
if (inScope.length === 0) {
|
|
2147
|
+
return [
|
|
2148
|
+
{
|
|
2149
|
+
lineName: `Parity: ${contract.id}`,
|
|
2150
|
+
verdict: {
|
|
2151
|
+
status: 'skip',
|
|
2152
|
+
message: contract.consumers !== undefined
|
|
2153
|
+
? `cohort permits absence here (no roster repo in consumers [${contract.consumers.join(', ')}])`
|
|
2154
|
+
: 'no roster repo resolved to probe (current-repo slug unresolvable)',
|
|
2155
|
+
},
|
|
2156
|
+
},
|
|
2157
|
+
];
|
|
2158
|
+
}
|
|
2159
|
+
switch (ctx.row) {
|
|
2160
|
+
case 'repo-merge-posture':
|
|
2161
|
+
return inScope.map((repo) => mergePostureLine(contract, repo));
|
|
2162
|
+
case 'repo-required-checks-posture':
|
|
2163
|
+
return requiredChecksLines(contract, ctx, inScope);
|
|
2164
|
+
case 'repo-branch-protection-posture':
|
|
2165
|
+
return inScope.flatMap((repo) => branchProtectionLines(contract, repo));
|
|
2166
|
+
default:
|
|
2167
|
+
// Defensive: an unrecognized row degrades to a single honest-absent skip
|
|
2168
|
+
// rather than darking the sensor (mirrors the manifestation fail-loud).
|
|
2169
|
+
return [
|
|
2170
|
+
{
|
|
2171
|
+
lineName: `Parity: ${contract.id}`,
|
|
2172
|
+
verdict: { status: 'skip', message: `network-posture row unrecognized by this doctor` },
|
|
2173
|
+
},
|
|
2174
|
+
];
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
/** True when `repoId` is inside the contract's `consumers` scope (undefined = applies to all). */
|
|
2178
|
+
function repoInConsumerScope(contract, repoId) {
|
|
2179
|
+
return contract.consumers === undefined || contract.consumers.includes(repoId);
|
|
2180
|
+
}
|
|
2181
|
+
/** Append ` (detail)` to a message when the snapshot carries render detail. */
|
|
2182
|
+
function detailSuffix(surface) {
|
|
2183
|
+
return surface.detail !== undefined && surface.detail.length > 0 ? ` (${surface.detail})` : '';
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Map a non-`ok` (or absent) surface to its cannot-verify verdict, or `undefined`
|
|
2187
|
+
* when the surface is `ok` and the caller should inspect the payload. §14 clause
|
|
2188
|
+
* 2/4: `no-transport` → `skip` (honest-absent), every other failure → `unknown`
|
|
2189
|
+
* (never a drift verdict).
|
|
2190
|
+
*/
|
|
2191
|
+
function surfaceCannotVerify(surface, surfaceLabel) {
|
|
2192
|
+
if (surface === undefined) {
|
|
2193
|
+
return { status: 'unknown', message: `${surfaceLabel}: not probed — cannot verify` };
|
|
2194
|
+
}
|
|
2195
|
+
switch (surface.outcome) {
|
|
2196
|
+
case 'ok':
|
|
2197
|
+
return undefined;
|
|
2198
|
+
case 'no-transport':
|
|
2199
|
+
return {
|
|
2200
|
+
status: 'skip',
|
|
2201
|
+
message: `${surfaceLabel}: no transport (gh unavailable / offline) — honest-absent per §14 clause 4${detailSuffix(surface)}`,
|
|
2202
|
+
};
|
|
2203
|
+
case 'auth':
|
|
2204
|
+
return {
|
|
2205
|
+
status: 'unknown',
|
|
2206
|
+
message: `${surfaceLabel}: auth-class — cannot verify (missing / under-privileged token; never posture-false per §14 clause 2)${detailSuffix(surface)}`,
|
|
2207
|
+
};
|
|
2208
|
+
case 'not-found':
|
|
2209
|
+
return {
|
|
2210
|
+
status: 'unknown',
|
|
2211
|
+
message: `${surfaceLabel}: 404 on a governed surface — indistinguishable from under-privilege; cannot verify per §14 clause 2${detailSuffix(surface)}`,
|
|
2212
|
+
};
|
|
2213
|
+
case 'error':
|
|
2214
|
+
return {
|
|
2215
|
+
status: 'unknown',
|
|
2216
|
+
message: `${surfaceLabel}: transient / unreachable — cannot verify${detailSuffix(surface)}`,
|
|
2217
|
+
};
|
|
2218
|
+
default:
|
|
2219
|
+
return {
|
|
2220
|
+
status: 'unknown',
|
|
2221
|
+
message: `${surfaceLabel}: unrecognized outcome — cannot verify`,
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
// ── Row 1: repo-merge-posture ──
|
|
2226
|
+
/**
|
|
2227
|
+
* One repo's merge-posture line: `GET /repos/{owner}/{repo}` must report
|
|
2228
|
+
* squash-only merges + a BLANK squash body + a PR_TITLE squash title. A 200
|
|
2229
|
+
* missing those fields is auth-class (`unknown`); a read mismatch is drift
|
|
2230
|
+
* (`warn`). Silent by choice on `delete_branch_on_merge`.
|
|
2231
|
+
*/
|
|
2232
|
+
function mergePostureLine(contract, repo) {
|
|
2233
|
+
const lineName = `Parity: ${contract.id} [${repo.repoSlug}]`;
|
|
2234
|
+
const surface = repo.surfaces.repoSettings;
|
|
2235
|
+
const cannot = surfaceCannotVerify(surface, 'repo settings');
|
|
2236
|
+
if (cannot !== undefined)
|
|
2237
|
+
return { lineName, verdict: cannot };
|
|
2238
|
+
const parsed = RepoMergeSettingsSchema.safeParse(surface?.data);
|
|
2239
|
+
if (!parsed.success) {
|
|
2240
|
+
return {
|
|
2241
|
+
lineName,
|
|
2242
|
+
verdict: {
|
|
2243
|
+
status: 'unknown',
|
|
2244
|
+
message: 'repo settings: 200 without the merge-posture fields — auth-class (under-privileged token), never posture-false (§14 clause 2)',
|
|
2245
|
+
},
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
const s = parsed.data;
|
|
2249
|
+
const drift = [];
|
|
2250
|
+
if (s.allow_squash_merge !== true)
|
|
2251
|
+
drift.push('allow_squash_merge≠true');
|
|
2252
|
+
if (s.allow_merge_commit !== false)
|
|
2253
|
+
drift.push('allow_merge_commit≠false');
|
|
2254
|
+
if (s.allow_rebase_merge !== false)
|
|
2255
|
+
drift.push('allow_rebase_merge≠false');
|
|
2256
|
+
if (s.squash_merge_commit_message !== EXPECTED_SQUASH_MESSAGE)
|
|
2257
|
+
drift.push(`squash_merge_commit_message=${s.squash_merge_commit_message}≠${EXPECTED_SQUASH_MESSAGE}`);
|
|
2258
|
+
if (s.squash_merge_commit_title !== EXPECTED_SQUASH_TITLE)
|
|
2259
|
+
drift.push(`squash_merge_commit_title=${s.squash_merge_commit_title}≠${EXPECTED_SQUASH_TITLE}`);
|
|
2260
|
+
if (drift.length > 0) {
|
|
2261
|
+
return {
|
|
2262
|
+
lineName,
|
|
2263
|
+
verdict: {
|
|
2264
|
+
status: 'warn',
|
|
2265
|
+
message: `merge posture drifted: ${drift.join(', ')}`,
|
|
2266
|
+
remediation: 'Restore squash-only merges with a BLANK squash body + PR_TITLE title in the repo Settings → General → Pull Requests.',
|
|
2267
|
+
},
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
return {
|
|
2271
|
+
lineName,
|
|
2272
|
+
verdict: { status: 'pass', message: 'squash-only + BLANK squash body + PR_TITLE title' },
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
// ── Row 2: repo-required-checks-posture ──
|
|
2276
|
+
/**
|
|
2277
|
+
* The required-checks lines: the active-ruleset UNION of `required_status_checks`
|
|
2278
|
+
* must set-equal the canonical list (BOTH directions), AND every ruleset
|
|
2279
|
+
* contributing a canonical check must itself hold enforcement=active,
|
|
2280
|
+
* target ~DEFAULT_BRANCH, `bypass_actors=[]`, and the pinned
|
|
2281
|
+
* `strict_required_status_checks_policy` — the union must not hide a bypassable
|
|
2282
|
+
* contributing ruleset. Canonical + pin come from `.totem/rulesets/main.json`;
|
|
2283
|
+
* an absent declaration is honest-absent `skip`.
|
|
2284
|
+
*/
|
|
2285
|
+
function requiredChecksLines(contract, ctx, inScope) {
|
|
2286
|
+
// ── Canonical declaration (read ONCE; absent → honest-absent skip) ──
|
|
2287
|
+
const readFile = ctx.readFile ?? readFileText;
|
|
2288
|
+
const declPath = ctx.declarationPath;
|
|
2289
|
+
const declRaw = declPath !== undefined ? safeReadFile(readFile, declPath) : undefined;
|
|
2290
|
+
if (declRaw === undefined) {
|
|
2291
|
+
return inScope.map((repo) => ({
|
|
2292
|
+
lineName: `Parity: ${contract.id} [${repo.repoSlug}]`,
|
|
2293
|
+
verdict: {
|
|
2294
|
+
status: 'skip',
|
|
2295
|
+
message: 'canonical ruleset declaration (.totem/rulesets/main.json) not yet committed — honest-absent (interim canonical is prose, never parser input)',
|
|
2296
|
+
},
|
|
2297
|
+
}));
|
|
2298
|
+
}
|
|
2299
|
+
const canonical = parseRulesetDeclaration(declRaw);
|
|
2300
|
+
if (canonical === undefined) {
|
|
2301
|
+
// Malformed / unsupported canonical: cannot prove drift NOR currency (the
|
|
2302
|
+
// Stale-Doctor-Paradox) → unknown, never a fabricated pass/warn.
|
|
2303
|
+
return inScope.map((repo) => ({
|
|
2304
|
+
lineName: `Parity: ${contract.id} [${repo.repoSlug}]`,
|
|
2305
|
+
verdict: {
|
|
2306
|
+
status: 'unknown',
|
|
2307
|
+
message: '.totem/rulesets/main.json is unparseable / missing required_status_checks.contexts — canonical list underivable, cannot verify',
|
|
2308
|
+
},
|
|
2309
|
+
}));
|
|
2310
|
+
}
|
|
2311
|
+
return inScope.map((repo) => requiredChecksLine(contract, repo, canonical));
|
|
2312
|
+
}
|
|
2313
|
+
/** Parse + narrow the declaration; undefined when unparseable / unsupported / context-less. */
|
|
2314
|
+
function parseRulesetDeclaration(raw) {
|
|
2315
|
+
let doc;
|
|
2316
|
+
try {
|
|
2317
|
+
doc = JSON.parse(raw);
|
|
2318
|
+
// totem-context: a malformed canonical declaration is a first-class "canonical underivable" signal (→ unknown), not a throw — the sensor must never crash on a mis-authored totem-side file.
|
|
2319
|
+
}
|
|
2320
|
+
catch {
|
|
2321
|
+
return undefined;
|
|
2322
|
+
}
|
|
2323
|
+
const parsed = RulesetDeclarationSchema.safeParse(doc);
|
|
2324
|
+
if (!parsed.success)
|
|
2325
|
+
return undefined;
|
|
2326
|
+
if (parsed.data['schema-version'] !== 1)
|
|
2327
|
+
return undefined;
|
|
2328
|
+
const contexts = parsed.data.required_status_checks?.contexts;
|
|
2329
|
+
if (contexts === undefined || contexts.length === 0)
|
|
2330
|
+
return undefined;
|
|
2331
|
+
return {
|
|
2332
|
+
contexts: new Set(contexts),
|
|
2333
|
+
// Pinned posture; the row's expected value is `false`, but the PIN is whatever
|
|
2334
|
+
// the canonical declares (derive-not-hardcode, Tenet 20).
|
|
2335
|
+
strictPolicy: parsed.data.required_status_checks?.strict_required_status_checks_policy ?? false,
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
/** One repo's required-checks line (union set-compare + per-contributing-ruleset enforcement). */
|
|
2339
|
+
function requiredChecksLine(contract, repo, canonical) {
|
|
2340
|
+
const lineName = `Parity: ${contract.id} [${repo.repoSlug}]`;
|
|
2341
|
+
const surface = repo.surfaces.rulesets;
|
|
2342
|
+
const cannot = surfaceCannotVerify(surface, 'rulesets');
|
|
2343
|
+
if (cannot !== undefined)
|
|
2344
|
+
return { lineName, verdict: cannot };
|
|
2345
|
+
const parsed = RulesetsArraySchema.safeParse(surface?.data);
|
|
2346
|
+
if (!parsed.success) {
|
|
2347
|
+
return {
|
|
2348
|
+
lineName,
|
|
2349
|
+
verdict: {
|
|
2350
|
+
status: 'unknown',
|
|
2351
|
+
message: 'rulesets: 200 with an unparseable body — cannot verify (§14 clause 2)',
|
|
2352
|
+
},
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
// Every ruleset targeting the default branch (any enforcement mode) is
|
|
2356
|
+
// considered for the union so a check supplied ONLY by an evaluate-mode ruleset
|
|
2357
|
+
// still appears in the set-compare — the per-contributing-ruleset gate below is
|
|
2358
|
+
// what catches the bypassable/evaluate case with a precise reason, rather than a
|
|
2359
|
+
// blunt "missing" (round finding + greptile P1 on strategy#962).
|
|
2360
|
+
const defaultRulesets = parsed.data.filter(rulesetTargetsDefault);
|
|
2361
|
+
const union = new Set();
|
|
2362
|
+
const contributing = [];
|
|
2363
|
+
for (const ruleset of defaultRulesets) {
|
|
2364
|
+
const contexts = statusCheckContexts(ruleset);
|
|
2365
|
+
if (contexts.length === 0)
|
|
2366
|
+
continue; // a zero-rule / copilot-class ruleset never satisfies presence
|
|
2367
|
+
for (const c of contexts)
|
|
2368
|
+
union.add(c);
|
|
2369
|
+
contributing.push({ ruleset, contexts });
|
|
2370
|
+
}
|
|
2371
|
+
const drift = [];
|
|
2372
|
+
// ── Set-compare BOTH directions ──
|
|
2373
|
+
const missing = [...canonical.contexts].filter((c) => !union.has(c));
|
|
2374
|
+
const extra = [...union].filter((c) => !canonical.contexts.has(c));
|
|
2375
|
+
if (missing.length > 0)
|
|
2376
|
+
drift.push(`missing required check(s): ${missing.join(', ')} (re-opens the gated vector)`);
|
|
2377
|
+
if (extra.length > 0)
|
|
2378
|
+
drift.push(`stale extra required check(s): ${extra.join(', ')} (silent merge-block)`);
|
|
2379
|
+
// ── Per-contributing-ruleset enforcement posture ──
|
|
2380
|
+
const unobserved = [];
|
|
2381
|
+
for (const { ruleset, contexts } of contributing) {
|
|
2382
|
+
const read = enforcementProblems(ruleset, canonical.strictPolicy);
|
|
2383
|
+
const name = ruleset.name ?? String(ruleset.id ?? '(unnamed)');
|
|
2384
|
+
if (read.problems.length > 0) {
|
|
2385
|
+
drift.push(`contributing ruleset '${name}' (supplies ${contexts.join(', ')}) is ${read.problems.join(' / ')}`);
|
|
2386
|
+
}
|
|
2387
|
+
if (read.unobserved.length > 0) {
|
|
2388
|
+
unobserved.push(`contributing ruleset '${name}' omitted ${read.unobserved.join(', ')}`);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
if (drift.length > 0) {
|
|
2392
|
+
return {
|
|
2393
|
+
lineName,
|
|
2394
|
+
verdict: {
|
|
2395
|
+
status: 'warn',
|
|
2396
|
+
message: `required-checks posture drifted: ${drift.join('; ')}`,
|
|
2397
|
+
remediation: 'Align the default-branch ruleset union to the canonical required-checks list and make every contributing ruleset enforcement=active with no bypass actors.',
|
|
2398
|
+
},
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
// Observed drift outranks an observability gap (a definite finding is never
|
|
2402
|
+
// hidden behind unknown); with no drift, a field-shy contributing ruleset is
|
|
2403
|
+
// auth-class — never a silent pass (§14 clause 2).
|
|
2404
|
+
if (unobserved.length > 0) {
|
|
2405
|
+
return {
|
|
2406
|
+
lineName,
|
|
2407
|
+
verdict: {
|
|
2408
|
+
status: 'unknown',
|
|
2409
|
+
message: `rulesets detail field-shy — cannot certify enforcement posture: ${unobserved.join('; ')} (auth-class, §14 clause 2)`,
|
|
2410
|
+
},
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
return {
|
|
2414
|
+
lineName,
|
|
2415
|
+
verdict: {
|
|
2416
|
+
status: 'pass',
|
|
2417
|
+
message: `active-ruleset union == canonical required checks (${canonical.contexts.size}); every contributing ruleset un-bypassable`,
|
|
2418
|
+
},
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
function enforcementProblems(ruleset, pinnedStrict) {
|
|
2422
|
+
const problems = [];
|
|
2423
|
+
const unobserved = [];
|
|
2424
|
+
if (ruleset.enforcement === undefined)
|
|
2425
|
+
unobserved.push('enforcement');
|
|
2426
|
+
else if (ruleset.enforcement !== 'active')
|
|
2427
|
+
problems.push(`enforcement=${ruleset.enforcement}`);
|
|
2428
|
+
if (ruleset.bypass_actors === undefined)
|
|
2429
|
+
unobserved.push('bypass_actors');
|
|
2430
|
+
else if (ruleset.bypass_actors.length > 0)
|
|
2431
|
+
problems.push('bypassable (bypass_actors non-empty)');
|
|
2432
|
+
// Row-3 callers omit the pin: protection rulesets are judged on enforcement +
|
|
2433
|
+
// bypass only — the strict policy is a required-checks (row-2) concern.
|
|
2434
|
+
if (pinnedStrict === undefined)
|
|
2435
|
+
return { problems, unobserved };
|
|
2436
|
+
const strict = strictPolicy(ruleset);
|
|
2437
|
+
if (strict === undefined)
|
|
2438
|
+
unobserved.push('strict_required_status_checks_policy');
|
|
2439
|
+
else if (strict !== pinnedStrict)
|
|
2440
|
+
problems.push(`strict_required_status_checks_policy=${strict}≠${pinnedStrict}`);
|
|
2441
|
+
return { problems, unobserved };
|
|
2442
|
+
}
|
|
2443
|
+
// ── Row 3: repo-branch-protection-posture ──
|
|
2444
|
+
/**
|
|
2445
|
+
* One repo's TWO branch-protection lines — classic branch protection AND the
|
|
2446
|
+
* rulesets surface (reading one senses a subset). Classic: PR-required with a
|
|
2447
|
+
* ruled `required_approving_review_count=0`, `enforce_admins=true`, force pushes
|
|
2448
|
+
* + deletions disallowed. Rulesets: any active ruleset carrying push/PR-class
|
|
2449
|
+
* rules for the default branch must be enforcement=active with `bypass_actors=[]`.
|
|
2450
|
+
*/
|
|
2451
|
+
function branchProtectionLines(contract, repo) {
|
|
2452
|
+
return [classicProtectionLine(contract, repo), rulesetProtectionLine(contract, repo)];
|
|
2453
|
+
}
|
|
2454
|
+
/** The classic-branch-protection line (row-3 surface 1). */
|
|
2455
|
+
function classicProtectionLine(contract, repo) {
|
|
2456
|
+
const lineName = `Parity: ${contract.id} [${repo.repoSlug} · classic]`;
|
|
2457
|
+
const surface = repo.surfaces.branchProtection;
|
|
2458
|
+
const cannot = surfaceCannotVerify(surface, 'classic branch protection');
|
|
2459
|
+
if (cannot !== undefined)
|
|
2460
|
+
return { lineName, verdict: cannot };
|
|
2461
|
+
const parsed = BranchProtectionSchema.safeParse(surface?.data);
|
|
2462
|
+
if (!parsed.success) {
|
|
2463
|
+
// The three toggles are always present in a full admin read; their absence
|
|
2464
|
+
// marks an under-privileged 200 → auth-class, never posture-false.
|
|
2465
|
+
return {
|
|
2466
|
+
lineName,
|
|
2467
|
+
verdict: {
|
|
2468
|
+
status: 'unknown',
|
|
2469
|
+
message: 'classic branch protection: 200 without the enforce_admins/force-push/deletion toggles — auth-class (§14 clause 2)',
|
|
2470
|
+
},
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
const p = parsed.data;
|
|
2474
|
+
const drift = [];
|
|
2475
|
+
if (p.required_pull_request_reviews === undefined) {
|
|
2476
|
+
drift.push('required_pull_request_reviews absent (PR not required — direct-push vector open)');
|
|
2477
|
+
}
|
|
2478
|
+
else {
|
|
2479
|
+
const count = p.required_pull_request_reviews.required_approving_review_count;
|
|
2480
|
+
if (count === undefined) {
|
|
2481
|
+
// reviews object present but count field shy → auth-class read.
|
|
2482
|
+
return {
|
|
2483
|
+
lineName,
|
|
2484
|
+
verdict: {
|
|
2485
|
+
status: 'unknown',
|
|
2486
|
+
message: 'classic branch protection: reviews object present without required_approving_review_count — auth-class (§14 clause 2)',
|
|
2487
|
+
},
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
if (count !== 0)
|
|
2491
|
+
drift.push(`required_approving_review_count=${count}≠0 (ruled posture — nonzero deadlocks the solo-operator merge)`);
|
|
2492
|
+
}
|
|
2493
|
+
if (p.enforce_admins.enabled !== true)
|
|
2494
|
+
drift.push('enforce_admins≠true');
|
|
2495
|
+
if (p.allow_force_pushes.enabled !== false)
|
|
2496
|
+
drift.push('allow_force_pushes≠false');
|
|
2497
|
+
if (p.allow_deletions.enabled !== false)
|
|
2498
|
+
drift.push('allow_deletions≠false');
|
|
2499
|
+
if (drift.length > 0) {
|
|
2500
|
+
return {
|
|
2501
|
+
lineName,
|
|
2502
|
+
verdict: {
|
|
2503
|
+
status: 'warn',
|
|
2504
|
+
message: `classic branch protection drifted: ${drift.join(', ')}`,
|
|
2505
|
+
remediation: 'Restore the default-branch protection: PR required with required_approving_review_count=0, enforce_admins on, force pushes + deletions off.',
|
|
2506
|
+
},
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
return {
|
|
2510
|
+
lineName,
|
|
2511
|
+
verdict: {
|
|
2512
|
+
status: 'pass',
|
|
2513
|
+
message: 'PR required (approving-count 0), enforce_admins on, force pushes + deletions off',
|
|
2514
|
+
},
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
/** The rulesets-surface line for row-3 (surface 2 — a bypassable protection ruleset must not undercut classic). */
|
|
2518
|
+
function rulesetProtectionLine(contract, repo) {
|
|
2519
|
+
const lineName = `Parity: ${contract.id} [${repo.repoSlug} · rulesets]`;
|
|
2520
|
+
const surface = repo.surfaces.rulesets;
|
|
2521
|
+
const cannot = surfaceCannotVerify(surface, 'rulesets');
|
|
2522
|
+
if (cannot !== undefined)
|
|
2523
|
+
return { lineName, verdict: cannot };
|
|
2524
|
+
const parsed = RulesetsArraySchema.safeParse(surface?.data);
|
|
2525
|
+
if (!parsed.success) {
|
|
2526
|
+
return {
|
|
2527
|
+
lineName,
|
|
2528
|
+
verdict: {
|
|
2529
|
+
status: 'unknown',
|
|
2530
|
+
message: 'rulesets: 200 with an unparseable body — cannot verify (§14 clause 2)',
|
|
2531
|
+
},
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
const protectionRulesets = parsed.data
|
|
2535
|
+
.filter(rulesetTargetsDefault)
|
|
2536
|
+
.filter((r) => (r.rules ?? []).some((rule) => PUSH_PR_RULE_TYPES.has(rule.type)));
|
|
2537
|
+
const drift = [];
|
|
2538
|
+
const unobserved = [];
|
|
2539
|
+
for (const ruleset of protectionRulesets) {
|
|
2540
|
+
const name = ruleset.name ?? String(ruleset.id ?? '(unnamed)');
|
|
2541
|
+
const read = enforcementProblems(ruleset);
|
|
2542
|
+
if (read.problems.length > 0)
|
|
2543
|
+
drift.push(`protection ruleset '${name}' is ${read.problems.join(' / ')}`);
|
|
2544
|
+
if (read.unobserved.length > 0)
|
|
2545
|
+
unobserved.push(`protection ruleset '${name}' omitted ${read.unobserved.join(', ')}`);
|
|
2546
|
+
}
|
|
2547
|
+
if (drift.length > 0) {
|
|
2548
|
+
return {
|
|
2549
|
+
lineName,
|
|
2550
|
+
verdict: {
|
|
2551
|
+
status: 'warn',
|
|
2552
|
+
message: `ruleset protection drifted: ${drift.join('; ')}`,
|
|
2553
|
+
remediation: 'Make every default-branch push/PR ruleset enforcement=active with no bypass actors so it cannot undercut classic protection.',
|
|
2554
|
+
},
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
// Same precedence as row 2: a field-shy protection ruleset is auth-class —
|
|
2558
|
+
// never certified un-bypassable without observing the field (§14 clause 2).
|
|
2559
|
+
if (unobserved.length > 0) {
|
|
2560
|
+
return {
|
|
2561
|
+
lineName,
|
|
2562
|
+
verdict: {
|
|
2563
|
+
status: 'unknown',
|
|
2564
|
+
message: `rulesets detail field-shy — cannot certify protection posture: ${unobserved.join('; ')} (auth-class, §14 clause 2)`,
|
|
2565
|
+
},
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
return {
|
|
2569
|
+
lineName,
|
|
2570
|
+
verdict: {
|
|
2571
|
+
status: 'pass',
|
|
2572
|
+
message: protectionRulesets.length === 0
|
|
2573
|
+
? 'no default-branch push/PR ruleset present to undercut classic protection'
|
|
2574
|
+
: `${protectionRulesets.length} default-branch push/PR ruleset(s) active + un-bypassable`,
|
|
2575
|
+
},
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
// ── Shared ruleset helpers ──
|
|
2579
|
+
/** True when a ruleset's ref-name conditions target the default branch (and don't exclude it). */
|
|
2580
|
+
function rulesetTargetsDefault(ruleset) {
|
|
2581
|
+
const refName = ruleset.conditions?.ref_name;
|
|
2582
|
+
if (refName === undefined)
|
|
2583
|
+
return false;
|
|
2584
|
+
const include = refName.include ?? [];
|
|
2585
|
+
const exclude = refName.exclude ?? [];
|
|
2586
|
+
const included = include.some((r) => DEFAULT_BRANCH_INCLUDES.has(r));
|
|
2587
|
+
const excluded = exclude.some((r) => DEFAULT_BRANCH_INCLUDES.has(r));
|
|
2588
|
+
return included && !excluded;
|
|
2589
|
+
}
|
|
2590
|
+
/** The `required_status_checks` contexts a ruleset supplies (empty when it carries no such rule). */
|
|
2591
|
+
function statusCheckContexts(ruleset) {
|
|
2592
|
+
const contexts = [];
|
|
2593
|
+
for (const rule of ruleset.rules ?? []) {
|
|
2594
|
+
if (rule.type !== 'required_status_checks')
|
|
2595
|
+
continue;
|
|
2596
|
+
const params = StatusCheckParamsSchema.safeParse(rule.parameters);
|
|
2597
|
+
if (!params.success)
|
|
2598
|
+
continue;
|
|
2599
|
+
for (const check of params.data.required_status_checks ?? [])
|
|
2600
|
+
contexts.push(check.context);
|
|
2601
|
+
}
|
|
2602
|
+
return contexts;
|
|
2603
|
+
}
|
|
2604
|
+
/** The `strict_required_status_checks_policy` a ruleset pins, or undefined when it carries no such rule. */
|
|
2605
|
+
function strictPolicy(ruleset) {
|
|
2606
|
+
for (const rule of ruleset.rules ?? []) {
|
|
2607
|
+
if (rule.type !== 'required_status_checks')
|
|
2608
|
+
continue;
|
|
2609
|
+
const params = StatusCheckParamsSchema.safeParse(rule.parameters);
|
|
2610
|
+
if (params.success)
|
|
2611
|
+
return params.data.strict_required_status_checks_policy;
|
|
2612
|
+
}
|
|
2613
|
+
return undefined;
|
|
2614
|
+
}
|
|
2615
|
+
/** Read a file through the injected seam, swallowing a throwing reader to undefined (honest-absent). */
|
|
2616
|
+
function safeReadFile(readFile, absPath) {
|
|
2617
|
+
try {
|
|
2618
|
+
return readFile(absPath);
|
|
2619
|
+
// totem-context: a throwing injected reader is the honest-absent signal (declaration file unreadable → skip); rethrowing would break the never-throws contract for a routine absence.
|
|
2620
|
+
}
|
|
2621
|
+
catch {
|
|
2622
|
+
return undefined;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2030
2625
|
//# sourceMappingURL=parity-detect.js.map
|