Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
cfe27b0
feat(lab): add CL-01 deterministic protocol conformance harness
Wibias Aug 8, 2026
574f1d5
fix(lab): align CL-01 harness with merged CL-00 #1286 contracts
Wibias Aug 8, 2026
22d608c
docs(lab): pin CL-01 contract-correction head SHA
Wibias Aug 8, 2026
d665004
docs(lab): sync CL-01 tip SHA after contract correction
Wibias Aug 8, 2026
cb4417d
fix(lab): harden CL-01 conformance review findings
Wibias Aug 8, 2026
5639c6c
fix(lab): close remaining Protocol V1 gaps
Wibias Aug 9, 2026
b16670f
fix(lab): harden tool-call projections
Wibias Aug 9, 2026
f5ddee4
fix(lab): fail closed on malformed controls
Wibias Aug 9, 2026
7814484
test(lab): cover review regression edges
Wibias Aug 9, 2026
5aab6d9
test(openai-chat): pin native CL-01 regressions
Wibias Aug 9, 2026
f79fc9e
fix(openai-chat): use native reasoning effort field
Wibias Aug 9, 2026
554f6f6
test(claude): pin initial failure framing
Wibias Aug 9, 2026
27aaaa4
docs(devlog): plan the vision sidecar model eligibility filter
lidge-jun Aug 9, 2026
b86b303
fix(claude): preserve initial error framing
Wibias Aug 9, 2026
ee5efaa
fix(lab): preserve nonstream fixture fallback
Wibias Aug 9, 2026
2868543
test(lab): preserve empty nonstream fixture fallback
Wibias Aug 9, 2026
3328f1b
fix(lab): isolate gateway reasoning fixture
Wibias Aug 9, 2026
b44a07a
fix(lab): correct gateway reasoning authority
Wibias Aug 9, 2026
0b85150
test(claude): cover created-then-failed framing
Wibias Aug 9, 2026
2f42ab3
feat(vision): filter sidecar describers by real image-input capability
lidge-jun Aug 9, 2026
a82977d
fix(claude): defer message start until semantic output
Wibias Aug 9, 2026
5308322
test(omp): isolate path contract temp homes
Wibias Aug 9, 2026
79831c9
Merge pull request #1323 from lidge-jun/maint/1109-omp-path-regression
Wibias Aug 9, 2026
3e5cd53
docs(devlog): release-train main-reconcile plan (260809)
lidge-jun Aug 9, 2026
73af78f
fix(ci): keep Ready blocked while deterministic hygiene fails
Wibias Aug 9, 2026
97aced7
fix(providers): surface OpenCode Zen short-window rate limits
Wibias Aug 9, 2026
6d7c26a
fix(docs): state Zen rate-limit header absence as conditional
Wibias Aug 9, 2026
9288916
test(openai-chat): retain fallback for untransportable tool images
Wibias Aug 9, 2026
13a20c3
fix(ci): address CodeRabbit findings on hygiene gate trust boundary
Wibias Aug 9, 2026
577819b
fix(openai-chat): preserve fallback for untransportable tool images
Wibias Aug 9, 2026
ab19769
Merge pull request #1329 from lidge-jun/fix/gate-hygiene-ready-coupling
Wibias Aug 9, 2026
b8d9036
docs(devlog): close release-train unit with v2.11.1 outcome record
lidge-jun Aug 9, 2026
af962ad
test(claude): keep idle pings scoped to semantic streams
Wibias Aug 9, 2026
15850a3
fix(providers): honor upstream Retry-After and scope retryOn429 tip
Wibias Aug 9, 2026
a51c549
Merge pull request #1330 from Wibias/wibias215/ocx-56-opencode-zen-ra…
Wibias Aug 9, 2026
4bb249b
Merge pull request #1320 from Wibias/feat/cl-01-conformance-harness
Wibias Aug 9, 2026
e8a21dc
fix(catalog): retain configured combo targets omitted by live discovery
Wibias Aug 9, 2026
dbdae1e
fix(catalog): retain combo-only targets missing from provider models
Wibias Aug 9, 2026
28bbad1
fix(catalog): avoid leaking combo-only targets into the public catalog
Wibias Aug 9, 2026
5a26fa1
fix(command-code): guard model alias lookups
luvs01 Aug 7, 2026
d519d5d
fix(routing): tolerate malformed historical attempts
luvs01 Aug 7, 2026
c9f2a58
test(routing): lock malformed 429 classification
luvs01 Aug 8, 2026
c8fddb8
fix(usage): bound startup hydration tail reads
luvs01 Aug 8, 2026
a3fdbb2
fix(routing): bound reasoning effort hydration
luvs01 Aug 7, 2026
c631c42
test(routing): strengthen trace hydration bounds
luvs01 Aug 8, 2026
b1b3134
test(routing): verify retained string truncation
luvs01 Aug 8, 2026
87aeeb7
fix(catalog): preserve nested reasoning efforts
dbc-hbin Aug 8, 2026
247d2d3
feat(lab): CL-02 immutable evidence ledger and SQLite projection
Wibias Aug 9, 2026
cb94cf0
docs(devlog): record CL-02 draft PR #1333 and implementation tip
Wibias Aug 9, 2026
07bc292
docs(devlog): point CL-02 stack tip at current branch HEAD
Wibias Aug 9, 2026
dd02029
fix(windows): hide npm launcher proxy child
Ingwannu Aug 8, 2026
d020662
fix(server): restrict plaintext sideband overrides to numeric loopback
luvs01 Aug 8, 2026
7f5f9c9
fix(codex): reject profile FIFOs without blocking
luvs01 Aug 8, 2026
9fc1d48
test(codex): cover vault FIFO replacement race
luvs01 Aug 8, 2026
d3da4f4
fix(management): reject null Claude toggle bodies
luvs01 Aug 8, 2026
8e6d661
fix(server): bound Claude Desktop apply bodies
luvs01 Aug 9, 2026
55c236b
fix(vision): resolve describer eligibility against the effective prov…
lidge-jun Aug 9, 2026
5168505
fix(catalog): apply combo retention on cache and flight boundaries
Wibias Aug 9, 2026
0ac7552
fix(vision): require a resolved Anthropic executor before offering it…
lidge-jun Aug 9, 2026
96ecd65
fix(lab): harden CL-02 evidence ledger per review findings
Wibias Aug 9, 2026
cf626d1
test(lab): add CL-02 review regression coverage and update status
Wibias Aug 9, 2026
cbcbbdd
feat(management): publish vision describers and refuse the blind ones
lidge-jun Aug 9, 2026
a9838c1
Merge pull request #1331 from Wibias/wibias215/ocx-111-bug-ocx-sync-d…
Ingwannu Aug 9, 2026
9532f2f
fix(management): judge describers against the executor that would run…
lidge-jun Aug 9, 2026
6196fc5
refactor(management): make the resolved vision executor an explicit a…
lidge-jun Aug 9, 2026
d6d50db
feat(gui): show only usable vision describers, in the delegation card…
lidge-jun Aug 9, 2026
afcac60
style(gui): give the web search sidecar the same card shell
lidge-jun Aug 9, 2026
88e3823
docs(devlog): plan the vision sidecar stack landing and triage its re…
lidge-jun Aug 9, 2026
db5e58e
fix(gui): save the backend the server supplied with each vision option
lidge-jun Aug 9, 2026
eebd9d4
Merge pull request #1326 from lidge-jun/codex/260809-vision-eligibili…
lidge-jun Aug 9, 2026
6babcfa
fix(codex): make history no-op detection atomic
luvs01 Aug 8, 2026
9b3d64d
fix(codex): address atomic no-op review
luvs01 Aug 8, 2026
c0e6512
fix(gui): carry the persisted backend onto the grandfathered vision o…
lidge-jun Aug 9, 2026
d4758bc
Merge pull request #1327 from lidge-jun/codex/260809-vision-sidecar-api
lidge-jun Aug 9, 2026
2495de3
fix(adapters): reject malformed nested response shapes
Ingwannu Aug 9, 2026
56cca2f
fix(deepseek): normalize tool result adjacency
Ingwannu Aug 9, 2026
e09fe6e
fix(vertex): scope replay by client thread
Ingwannu Aug 9, 2026
21c7157
fix(gui): treat an empty vision option list as authoritative
lidge-jun Aug 9, 2026
226b60f
docs(devlog): close the vision sidecar model filter unit
lidge-jun Aug 9, 2026
1251d31
fix(lab): close CL-02 phase-2 independent-review blockers
Wibias Aug 9, 2026
d685362
fix(gui): let the sidecar controls wrap once the card stacks
lidge-jun Aug 9, 2026
00785fd
fix(lab): reject symlink targets before artifact create
Wibias Aug 9, 2026
31954a9
chore: bump react-doctor to 0.9.11 and clear new findings
Wibias Aug 9, 2026
afc6697
fix(lab): lstat symlink squatters before artifact create
Wibias Aug 9, 2026
46f8f86
test(gui): update assertions for navigateHash provider links
Wibias Aug 9, 2026
e96a81b
Merge pull request #1328 from lidge-jun/codex/260809-vision-sidecar-card
lidge-jun Aug 9, 2026
6842a06
docs(devlog): record the layer-3 merge commit in the vision filter ou…
lidge-jun Aug 9, 2026
ef8b242
test(catalog): retry vacuous post-approval seam races on lock loss
Wibias Aug 9, 2026
40725d6
fix(lab): centralize evidence producer version
Wibias Aug 9, 2026
1eb173d
fix(lab): fail closed when sanitizing evidence
Wibias Aug 9, 2026
f7c0a8c
fix(lab): enforce restricted state directories
Wibias Aug 9, 2026
0afa852
fix(lab): validate suite manifest authority
Wibias Aug 9, 2026
527d97f
fix(lab): bound invalidation target lookup
Wibias Aug 9, 2026
4f6f9ab
fix(lab): make purge artifact retention fail closed
Wibias Aug 9, 2026
21906ac
fix(lab): constrain projection enum columns
Wibias Aug 9, 2026
2af46f3
fix(lab): make sensitive purge progress explicit
Wibias Aug 9, 2026
a12a555
fix(lab): complete ledger appends across short writes
Wibias Aug 9, 2026
9970fe2
Merge pull request #1336 from Wibias/wibias215/chore-bump-react-docto…
Wibias Aug 9, 2026
3dddcda
fix(lab): reject POSIX paths before ledger admission
Wibias Aug 9, 2026
f3d878a
fix(lab): verify artifact digests by class
Wibias Aug 9, 2026
441b782
fix(lab): persist real conformance evidence metadata
Wibias Aug 9, 2026
dca7624
fix(lab): enforce verification contracts and freshness
Wibias Aug 9, 2026
6ed1838
fix(lab): make verdict projection contract-safe
Wibias Aug 9, 2026
0644000
fix(lab): rebuild projection atomically
Wibias Aug 9, 2026
9ba59cd
fix(lab): accept measured runner timestamps
Wibias Aug 9, 2026
6d79099
Merge pull request #1340 from lidge-jun/codex/260809-vision-outcome-m…
lidge-jun Aug 9, 2026
1ac3d32
test(lab): cover phase-2 evidence fixes
Wibias Aug 9, 2026
daab377
fix(lab): classify artifact filesystem failures
Wibias Aug 9, 2026
e5af56a
fix(lab): remove raw ledger production reader
Wibias Aug 9, 2026
02ca5b5
docs(lab): document sensitive purge exception
Wibias Aug 9, 2026
20969a6
fix(responses): bound synthesized SSE expansion
luvs01 Aug 7, 2026
3bfe807
test(responses): target synthetic bounded-json fixture
lidge-jun Aug 9, 2026
ba52150
fix(subagent): move per-role model fallback into opencodex config (#1…
Yuxin-Qiao Aug 7, 2026
66ba021
fix(doctor): flag model_fallback key presence, including empty arrays
Yuxin-Qiao Aug 8, 2026
81cebba
fix(doctor): make model_fallback detection TOML-aware
Yuxin-Qiao Aug 8, 2026
08ecdba
fix(doctor): handle escaped delimiters and strict array commas
Yuxin-Qiao Aug 8, 2026
d75743a
fix(doctor): reject trailing tokens after the model_fallback array
Yuxin-Qiao Aug 8, 2026
fc17327
fix(subagent): place legacy fallback after global config
lidge-jun Aug 9, 2026
a1e4185
fix(providers): correct Qwen3.8 reasoning levels
Aug 8, 2026
72ff55d
fix(providers): preserve Qwen3.8 user overrides
lidge-jun Aug 9, 2026
d6d3878
fix(ci): require aggregate check evidence
luvs01 Aug 8, 2026
1e07b0e
fix(ci): fail closed on readiness evidence
luvs01 Aug 8, 2026
817192f
fix(openai-chat): inject reasoning placeholder when replay cache misses
Yuxin-Qiao Aug 7, 2026
cd73c15
fix(openai-chat): scope reasoning placeholder to models that require it
Yuxin-Qiao Aug 7, 2026
c528847
fix(openai-chat): gate orphan placeholder on preserve list, keep opt-…
Yuxin-Qiao Aug 8, 2026
5cd3edb
test(auth): cover reasoning placeholder config boundaries
lidge-jun Aug 9, 2026
eecec3a
refactor(lab): isolate validation error type
Wibias Aug 9, 2026
0c9dc4d
refactor(lab): break validation import cycle
Wibias Aug 9, 2026
1eed4ff
fix(lab): validate claim source event ids
Wibias Aug 9, 2026
d3015cb
fix(cli): check live proxy before journal recovery
Ingwannu Aug 8, 2026
700803f
fix(cli): share proxy ownership preflight
lidge-jun Aug 9, 2026
96141e8
fix(ci): preserve code while normalizing issue media
Ingwannu Aug 8, 2026
d493c14
fix(ci): narrow media placeholder stripping
lidge-jun Aug 9, 2026
025c379
Merge pull request #1333 from Wibias/feat/cl-02-evidence-ledger
Wibias Aug 9, 2026
c27cafb
fix(routing): define hard cost-cap behavior when cost evidence is unk…
Aug 9, 2026
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
7 changes: 6 additions & 1 deletion .github/scripts/enforce-pr-target.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ describe("enforce-pr-target workflow", () => {
assert.match(workflow, /synchronize/);
});

it("uses label events for GUI waivers and a trusted CodeRabbit status signal", () => {
it("uses label events for GUI waivers, hygiene sponsorship, and a trusted CodeRabbit status signal", () => {
assert.doesNotMatch(workflow, /^ issue_comment:/m);
assert.match(workflow, /- labeled/);
assert.match(workflow, /- unlabeled/);
assert.match(workflow, /^ status:/m);
assert.match(workflow, /github\.event\.context == 'CodeRabbit'/);
assert.match(workflow, /github\.event\.state == 'success'/);
assert.match(workflow, /github\.event\.label\.name == 'gui-screenshot-waived'/);
assert.match(workflow, /github\.event\.label\.name == 'intake: hygiene-blocked'/);
assert.match(workflow, /github\.event\.label\.name == 'maintainer-sponsored'/);
assert.match(workflow, /listPullRequestsAssociatedWithCommit/);
assert.match(workflow, /candidate\.head\?\.sha === statusSha/);
assert.match(workflow, /candidates\.length !== 1/);
Expand Down Expand Up @@ -180,6 +182,9 @@ describe("enforce-pr-target workflow", () => {
it("loads pr-quality via require from the checked-out scripts", () => {
assert.match(workflow, /pr-quality\.cjs/);
assert.match(workflow, /collectPrQualityFailures/);
assert.match(workflow, /pr-hygiene\.cjs/);
assert.match(workflow, /collectDeterministicHygieneFailures/);
assert.match(workflow, /pulls\.listFiles/);
});

it("checks stacked bases via open PR heads before wrong_base enforcement", () => {
Expand Down
110 changes: 88 additions & 22 deletions .github/scripts/issue-quality-core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,45 +66,104 @@ function isPlaceholderOnlyValue(raw) {
*/
function stripMediaTokens(text) {
if (typeof text !== "string") return "";
// Indented code lines render as literal code in GitHub Markdown. Protect
// them first so neither the HTML nor the Markdown media stripper can
// remove example syntax; restore the lines afterwards.
// Fenced and indented code render literally in GitHub Markdown. Protect
// them first so neither media stripper can remove example syntax. The
// protector deliberately leaves indented children of an unindented HTML
// media block visible: those lines are HTML children, not Markdown code.
const protectedText = protectIndentedCodeLines(text);
const markdownStripped = stripMarkdownImages(stripHtmlMedia(protectedText.text));
const referenceStripped = stripReferenceImages(markdownStripped);
return restoreIndentedCodeLines(referenceStripped, protectedText.lines);
return restoreIndentedCodeLines(referenceStripped, protectedText);
}

/**
* Replace every indented code line (4+ leading spaces or a tab) with a
* placeholder of equal length so media stripping cannot touch it. Returns the
* masked text plus the original lines for restoration.
* Replace fenced code and indented code outside HTML media blocks with opaque
* tokens. Restoration is token-based rather than line-position-based because
* stripping a multiline media block may collapse or remove lines.
*/
function protectIndentedCodeLines(text) {
const lines = [];
let markerPrefix = "\u0000OCX_ISSUE_CODE_";
while (text.includes(markerPrefix)) markerPrefix += "_";
let mediaDepth = 0;
let pendingMediaTag = null;
let fence = null;

const mask = (line) => {
const index = lines.push(line) - 1;
return `${markerPrefix}${index}\u0000`;
};

const masked = text.split("\n").map((line) => {
if (/^(?: {4,}|\t)/.test(line)) {
lines.push(line);
return "\u0000" + line.replace(/[^\n]/g, " ").slice(1);
if (fence) {
const closing = new RegExp(`^ {0,3}${fence.char}{${fence.length},}[ \\t]*$`);
if (closing.test(line)) fence = null;
return mask(line);
}
lines.push(null);

const fenceStart = line.match(/^ {0,3}(`{3,}|~{3,})/);
if (fenceStart) {
fence = { char: fenceStart[1][0], length: fenceStart[1].length };
return mask(line);
}

// Four-space/tab lines inside an active unindented HTML media block are
// child markup or fallback text. Treating them as code would keep an
// otherwise media-only <picture>/<video> block alive.
if (mediaDepth === 0 && pendingMediaTag === null && /^(?: {4,}|\t)/.test(line)) {
return mask(line);
}

const mediaScan = htmlMediaDepthDelta(line, pendingMediaTag);
mediaDepth = Math.max(0, mediaDepth + mediaScan.delta);
pendingMediaTag = mediaScan.pendingTag;
return line;
});
return { text: masked.join("\n"), lines };
return { text: masked.join("\n"), lines, markerPrefix };
}

/**
* Restore masked indented-code lines from their original content. Placeholder
* lines are identified by the leading \u0000 marker and matched positionally.
* Count opening/closing block-media tags while carrying an unfinished opening
* tag across lines until `>`. Self-closing tags do not create a block. This
* scanner only decides whether indentation belongs to HTML; stripHtmlMedia
* remains the authority for removing media.
*/
function restoreIndentedCodeLines(text, lines) {
const out = text.split("\n").map((line, i) => {
if (lines[i] !== null && line.startsWith("\u0000")) {
return lines[i];
function htmlMediaDepthDelta(line, pendingTag = null) {
let delta = 0;
let cursor = 0;

const completeTag = (tag) => {
if (/^<\s*\//.test(tag)) delta -= 1;
else if (!/\/\s*>$/.test(tag)) delta += 1;
};

if (pendingTag !== null) {
const end = line.indexOf(">");
if (end === -1) return { delta, pendingTag: `${pendingTag}\n${line}` };
completeTag(`${pendingTag}\n${line.slice(0, end + 1)}`);
pendingTag = null;
cursor = end + 1;
}

const opening = /<(\/)?(picture|video|audio)\b/gi;
opening.lastIndex = cursor;
for (let match = opening.exec(line); match !== null; match = opening.exec(line)) {
const end = line.indexOf(">", opening.lastIndex);
if (end === -1) {
pendingTag = line.slice(match.index);
break;
}
return line;
});
return out.join("\n");
completeTag(line.slice(match.index, end + 1));
opening.lastIndex = end + 1;
}
return { delta, pendingTag };
}

/** Restore protected code from ordered tokens, independent of line count. */
function restoreIndentedCodeLines(text, protection) {
const escapedPrefix = protection.markerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const token = new RegExp(`${escapedPrefix}(\\d+)\\u0000`, "g");
return text.replace(token, (_match, rawIndex) => protection.lines[Number(rawIndex)] ?? "");
}

/**
Expand All @@ -131,12 +190,19 @@ function stripHtmlMedia(text) {
.replace(/<[^>]+>/g, " ")
.replace(/[\s_*~`]+/g, " ")
.trim();
return innerStripped.length === 0 ? " " : match;
// Only GitHub's exact generated "No response" value is empty here.
// Whole-field stand-ins such as TBD/N/A/None are valid media captions.
return innerStripped.length === 0 || isGitHubNoResponseMediaPlaceholder(inner) ? " " : match;
},
);
return s;
}

function isGitHubNoResponseMediaPlaceholder(inner) {
const text = inner.replace(/<[^>]+>/g, " ");
return /^[\s_*~`]*No response[\s_*~`]*$/.test(text);
}

/**
* Remove Markdown image tokens `![alt](dest "title")` using a small
* balanced scanner instead of a regex, because destinations may contain
Expand Down
59 changes: 59 additions & 0 deletions .github/scripts/issue-quality.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,71 @@ describe("validateIssue - feature", () => {
false,
);
assert.equal(isMediaOnly('<picture><source srcset="x.webp"><img src="x.png"></picture>'), true);
assert.equal(isMediaOnly('<video>No response</video>'), true);
assert.equal(isMediaOnly('<audio> _No response_ </audio>'), true);
assert.equal(isMediaOnly('<video><p><em>No response</em></p></video>'), true);
assert.equal(clean('<video>No response</video>'), "");
for (const caption of [
"TBD",
"N/A",
"None",
"설명 없음",
"🎬",
"demo.mp4",
"https://example.com/demo.mp4",
]) {
const media = `<video>${caption}</video>`;
assert.equal(stripMediaTokens(media), media, `caption must survive: ${caption}`);
assert.equal(isMediaOnly(media), false, `caption must be substantive: ${caption}`);
assert.equal(clean(media), media, `caption must survive cleaning: ${caption}`);
}
assert.equal(
isMediaOnly('<picture>\n <source srcset="x.webp">\n <img src="x.png">\n</picture>'),
true,
);
assert.equal(
isMediaOnly('<video>\n <source src="clip.mp4">\n Real fallback caption\n</video>'),
false,
);
assert.equal(
isMediaOnly([
"<picture",
' data-kind="responsive"',
">",
' <source srcset="x.webp">',
' <img src="x.png">',
"</picture>",
].join("\n")),
true,
);
assert.equal(isMediaOnly('<video src="clip.mp4"></video>'), true);
assert.equal(isMediaOnly('<img src="x.png" />\nCaption text'), false);
assert.equal(isMediaOnly("Some real description."), false);
assert.equal(stripMediaTokens('<img src="x.png" />').trim(), "");
assert.equal(stripMediaTokens('![alt](url "title")').trim(), "");
assert.equal(stripMediaTokens('before ![alt](url) after').replace(/\s+/g, " ").trim(), "before after");

const fencedMediaExample = [
"```html",
"<video>No response</video>",
"```",
].join("\n");
assert.equal(stripMediaTokens(fencedMediaExample), fencedMediaExample);
assert.equal(isMediaOnly(fencedMediaExample), false);

const protectedAroundMedia = [
" ![before](url)",
"<video>",
' <source src="clip.mp4">',
"</video>",
" ![after](url)",
].join("\n");
const strippedAroundMedia = stripMediaTokens(protectedAroundMedia);
assert.ok(strippedAroundMedia.includes(" ![before](url)"));
assert.ok(strippedAroundMedia.includes(" ![after](url)"));
assert.equal(strippedAroundMedia.includes("<video>"), false);
assert.equal(strippedAroundMedia.includes("<source"), false);
assert.equal(strippedAroundMedia.includes("\u0000"), false);
});

it("accepts a concise but actionable feature", () => {
Expand Down
69 changes: 69 additions & 0 deletions .github/scripts/pr-hygiene.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict";

const { assessSponsoredSurface } = require("./pr-sponsored-surface.cjs");

const GENERATED_PREFIXES = [
"gui/dist/",
"dist/",
Expand Down Expand Up @@ -218,9 +220,76 @@ function assessHygiene({ files = [], labels = [] }) {
return failures;
}

/**
* Human-readable one-liners for each deterministic hygiene failure code.
* Shared by the hygiene workflow comment and the PR quality gate actions.
*/
const HYGIENE_FAILURE_HINTS = {
missing_regression_test:
"Behavior changed under `src/` or `gui/src/` without a test change. Add focused coverage or obtain `test-exception-approved`.",
generated_output:
"Generated build output is committed. Remove it or obtain `generated-change-approved`.",
orphan_lockfile:
"`bun.lock` changed without `package.json`. Revert accidental churn or obtain `dependency-change-approved`.",
new_suppression:
"A new TypeScript, lint, formatter, or similar suppression was added. Fix the underlying issue or obtain `suppression-approved`.",
focused_or_skipped_test:
"A focused or skipped test was added. Restore the complete suite or obtain `test-exception-approved`.",
empty_catch:
"An empty catch block was added. Handle, report, or deliberately propagate the error.",
unsponsored_surface:
"This changes an authentication, workflow, release-automation, or dependency surface. `MAINTAINERS.md` requires security review for these; ask a maintainer to apply `maintainer-sponsored` once they have reviewed it.",
};

/**
* Labels that can clear or reinstate a hygiene failure. The quality gate must
* wake on these so READY / DRAFT tracks sponsorship and exception approvals
* without waiting for an unrelated synchronize.
*/
const HYGIENE_GATE_LABELS = [
"intake: hygiene-blocked",
"maintainer-sponsored",
"test-exception-approved",
"suppression-approved",
"generated-change-approved",
"dependency-change-approved",
];

/**
* Combine patch-hygiene and sponsored-surface failures into one list so the
* hygiene workflow and the PR quality gate cannot disagree about Ready.
*/
function collectDeterministicHygieneFailures({
files = [],
labels = [],
authorHasPushPermission = false,
}) {
// Renames must keep the source path: moving a restricted file to a
// non-restricted destination must not drop the sponsorship requirement.
const changedFiles = [
...new Set(
files.flatMap((file) => [
file.filename,
...(file.previous_filename ? [file.previous_filename] : []),
]),
),
];
return [
...assessHygiene({ files, labels }),
...assessSponsoredSurface({
authorHasPushPermission,
changedFiles,
labels,
}),
];
}

module.exports = {
addedLines,
assessHygiene,
collectDeterministicHygieneFailures,
HYGIENE_FAILURE_HINTS,
HYGIENE_GATE_LABELS,
hasEmptyCatch,
hasDeletions,
isBehaviorPath,
Expand Down
Loading
Loading