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
18 changes: 18 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,24 @@ export {
type LocalWriteActionSpec,
type LocalWriteJsonValue,
} from "./miner/local-write-tools.js";
export {
GENERIC_COMMIT_PATTERN,
SLOP_RUBRIC_MARKDOWN,
SLOP_WEIGHTS,
buildDuplicateClusterFinding,
buildEmptyDescriptionFinding,
buildLowQualityCommitMessageFinding,
buildMissingTestEvidenceFinding,
buildNoLinkedIssueRationaleFinding,
buildNonSubstantivePaddingFinding,
buildSlopAssessment,
buildTrivialWhitespaceChurnFinding,
hasClearNoIssueRationale,
type SlopAssessment,
type SlopAssessmentInput,
type SlopBand,
type SlopChangedFile,
} from "./signals/slop.js";
export * from "./plan-export.js";
export { countPlanStepsByStatus } from "./plan-step-stats.js";
export { countPlanSteps } from "./plan-step-count.js";
Expand Down
391 changes: 391 additions & 0 deletions packages/gittensory-engine/src/signals/slop.ts

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/gittensory-miner/lib/slop-assessment.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { SlopAssessment, SlopAssessmentInput } from "@jsonbored/gittensory-engine";

export function runSlopAssessment(input: SlopAssessmentInput): SlopAssessment;
20 changes: 20 additions & 0 deletions packages/gittensory-miner/lib/slop-assessment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { buildSlopAssessment } from "@jsonbored/gittensory-engine";

// Production runSlopAssessment binding (#5133, Wave 3.5 follow-up to #2334). `attempt-runner.js`'s
// `deps.runSlopAssessment` (via #2333's iterate-loop -> self-review-adapter's `SelfReviewAdapterDeps`) had
// no production implementation anywhere in this package -- only the test double in
// `test/unit/miner-attempt-runner.test.ts` existed. `packages/gittensory-engine/src/miner/self-review-adapter.ts`'s
// own header comment already anticipated this exact binding: `SelfReviewSlopInput`/`SelfReviewSlopAssessment`
// are a deliberate, hand-kept STRUCTURAL MIRROR of `buildSlopAssessment`'s own `SlopAssessmentInput`/
// `SlopAssessment` (down to reusing the SAME canonical `AdvisoryFinding` type for `findings`), specifically so
// a real binding could be a direct pass-through with no mapping logic once the deterministic scorer itself
// became portable -- which #5133 did (`src/signals/slop.ts`'s PR-side scorer is now extracted to
// `packages/gittensory-engine/src/signals/slop.ts`, byte-parity-verified against the live gate's own copy).

/**
* @param {import("@jsonbored/gittensory-engine").SlopAssessmentInput} input
* @returns {import("@jsonbored/gittensory-engine").SlopAssessment}
*/
export function runSlopAssessment(input) {
return buildSlopAssessment(input);
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"expected-engine.version"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-construction.js && node --check lib/coding-agent-house-rules.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/feasibility-cli.js && node --check lib/gate-verdict-poller.js && node --check lib/governor-action-mode.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-open-pr.js && node --check lib/governor-run-halt.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.js && node --check lib/laptop-init.js && node --check lib/local-store.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-snapshot.js && node --check lib/replay-task-generation.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js"
"build": "node --check bin/gittensory-miner.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-construction.js && node --check lib/coding-agent-house-rules.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/feasibility-cli.js && node --check lib/gate-verdict-poller.js && node --check lib/governor-action-mode.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-open-pr.js && node --check lib/governor-run-halt.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.js && node --check lib/laptop-init.js && node --check lib/local-store.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-snapshot.js && node --check lib/replay-task-generation.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/slop-assessment.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js"
},
"dependencies": {
"@jsonbored/gittensory-engine": "*"
Expand Down
3 changes: 2 additions & 1 deletion src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ import { attachDataQuality, buildCoreSignalFidelity, buildFreshnessSloReport, bu
import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor";
import { buildPullRequestReviewability, type PullRequestReviewability } from "../signals/reward-risk";
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { buildSlopAssessment, buildIssueSlopAssessment, SLOP_RUBRIC_MARKDOWN, ISSUE_SLOP_RUBRIC_MARKDOWN } from "../signals/slop";
import { buildIssueSlopAssessment, ISSUE_SLOP_RUBRIC_MARKDOWN } from "../signals/issue-slop";
import { buildSlopAssessment, SLOP_RUBRIC_MARKDOWN } from "../signals/slop";
import { buildPredictedGateVerdict } from "../rules/predicted-gate";
import { computeContributorCalibration } from "../review/predicted-gate-calibration-ledger";
import { buildFocusManifestValidation } from "../services/focus-manifest-validation";
Expand Down
3 changes: 2 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ import { resolveRepositorySettings } from "../settings/repository-settings";
import { MAX_FOCUS_MANIFEST_BYTES } from "../signals/focus-manifest";
import { loadPublicRepoFocusManifest, loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { buildPredictedGateVerdict, type PredictedGateVerdict } from "../rules/predicted-gate";
import { buildIssueSlopAssessment, buildSlopAssessment } from "../signals/slop";
import { buildIssueSlopAssessment } from "../signals/issue-slop";
import { buildSlopAssessment } from "../signals/slop";
import { buildStructuralImprovementAssessment } from "../signals/improvement";
import { buildBoundaryTestGenerationFinding, buildBoundaryTestGenerationSpec } from "../signals/boundary-test-generation";
import { buildRepoDataQuality } from "../signals/data-quality";
Expand Down
7 changes: 2 additions & 5 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,11 +461,8 @@ import {
type CheckFailureDetail,
type MergeReadiness,
} from "../review/unified-comment";
import {
buildIssueSlopAssessment,
buildSlopAssessment,
type SlopBand,
} from "../signals/slop";
import { buildIssueSlopAssessment } from "../signals/issue-slop";
import { buildSlopAssessment, type SlopBand } from "../signals/slop";
import { buildStructuralImprovementAssessment } from "../signals/improvement";
import { runGittensoryLinkedIssueSatisfaction } from "../services/linked-issue-satisfaction-run";
import { decidePublicSurface } from "../signals/settings-preview";
Expand Down
160 changes: 160 additions & 0 deletions src/signals/issue-slop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Issue-side slop triage (#533). Split out of src/signals/slop.ts (#5133 follow-up) so that file can be a
// pure re-export shim over packages/gittensory-engine/src/signals/slop.ts — the engine-parity checker
// (scripts/check-engine-parity.ts) only recognizes a host file as a shim when it contains NOTHING but the
// re-export statement, so this issue-side code (never extracted to the engine; not needed by the miner's
// self-review path) needed its own file. Reuses slop.ts's re-exported clamp/slopBandFor so the two sides
// continue to share identical band math.
import { clamp, slopBandFor, type SlopAssessment } from "./slop";
import type { SignalFinding } from "./engine";

// ─── Issue-side slop triage (#533) ──────────────────────────────────────────────────────────────────
// Advisory-only maintainer triage signal for low-effort issues — there is no issue gate, so these never
// block. High-precision signals only (an empty issue body is sometimes legitimate, so the bar is set at
// "clearly low-effort": empty body, or a template opened and submitted without being filled in).

export type IssueSlopAssessmentInput = {
title?: string | null | undefined;
body?: string | null | undefined;
};

export const ISSUE_SLOP_WEIGHTS = {
unfilledTemplate: 50,
emptyBody: 40,
titleRestatement: 35,
} as const;

export const ISSUE_SLOP_RUBRIC_MARKDOWN = [
"# Gittensory issue slop triage rubric",
"",
"- `clean`: 0",
"- `low`: 1-30",
"- `elevated`: 31-59",
"- `high`: 60-100",
"",
"Advisory-only (issues never block). Current deterministic signals:",
"- empty issue body",
"- issue template opened but left unfilled",
"- issue body only restates the title (no added detail)",
].join("\n");

export function buildIssueSlopAssessment(input: IssueSlopAssessmentInput): SlopAssessment {
const findings: SignalFinding[] = [];
const emptyBodyFinding = buildEmptyIssueBodyFinding(input);
// An empty body and an unfilled template are mutually exclusive (the latter needs a non-empty body), so
// only probe for the template when there IS a body to inspect.
const unfilledTemplateFinding = emptyBodyFinding ? null : buildUnfilledIssueTemplateFinding(input);
// The title-restatement signal needs a body with REAL prose (so it survives the unfilled-template strip),
// so it can only fire once the two emptier signals are ruled out — the three are mutually exclusive.
const titleRestatementFinding = emptyBodyFinding || unfilledTemplateFinding ? null : buildTitleRestatementIssueFinding(input);
if (unfilledTemplateFinding) findings.push(unfilledTemplateFinding);
if (emptyBodyFinding) findings.push(emptyBodyFinding);
if (titleRestatementFinding) findings.push(titleRestatementFinding);

const slopRisk = clamp(
(emptyBodyFinding ? ISSUE_SLOP_WEIGHTS.emptyBody : 0) +
(unfilledTemplateFinding ? ISSUE_SLOP_WEIGHTS.unfilledTemplate : 0) +
(titleRestatementFinding ? ISSUE_SLOP_WEIGHTS.titleRestatement : 0),
0,
100,
);
return { slopRisk, band: slopBandFor(slopRisk), findings };
}

export function buildEmptyIssueBodyFinding(input: IssueSlopAssessmentInput): SignalFinding | null {
if ((input.body ?? "").trim().length > 0) return null;
// Static, public-safe text (no interpolation) — no sanitizer guard needed, unlike the PR findings.
const detail = "This issue was opened with an empty body.";
return {
code: "empty_issue_body",
title: "Issue has no description",
severity: "warning",
detail,
action: "Add a clear description: what is wrong, where, and why it matters.",
publicText: detail,
};
}

// Fires when a non-empty body reduces to NOTHING substantive after stripping template scaffolding (HTML
// comments, markdown headings, empty bullets/checkboxes, residual punctuation) — i.e. the submitter opened
// the issue template and submitted it without filling anything in. Any real prose survives the strip → no fire.
export function buildUnfilledIssueTemplateFinding(input: IssueSlopAssessmentInput): SignalFinding | null {
const body = (input.body ?? "").trim();
if (body.length === 0) return null;
const substantive = stripHtmlComments(body) // HTML comment placeholders
.replace(/^#{1,6}\s.*$/gm, "") // markdown heading lines
.replace(/^\s*[-*]\s*(\[[ xX]\])?\s*$/gm, "") // empty bullets / checkboxes
.replace(/[\s>#*_`+-]/g, "") // residual markdown punctuation + whitespace
.trim();
// Require a real WORD (a run of 3+ letters/digits, any script) to survive — not merely "any surviving char",
// which a single padding character would satisfy to dodge the finding. (#audit-§4)
if (/[\p{L}\p{N}]{3,}/u.test(substantive)) return null;
// Static, public-safe text (no interpolation) — no sanitizer guard needed.
const detail = "The issue body contains only an unfilled template (headings or comment placeholders, no details).";
return {
code: "unfilled_issue_template",
title: "Issue template left unfilled",
severity: "warning",
detail,
action: "Fill in the template sections with the actual problem details.",
publicText: detail,
};
}

// Normalize for restatement comparison: lowercase, then collapse every run of non-alphanumeric characters
// (punctuation, markdown, whitespace, emoji) to a single space. This makes "Login is BROKEN!" and
// "login is broken" compare equal, so reformatting/punctuation alone cannot dodge the signal.
function normalizeIssueText(text: string): string {
return text
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, " ")
.trim();
}

// Fires when a non-empty body adds NOTHING beyond the title — it normalizes to exactly the title (a verbatim
// restatement or the title pasted back as the "description"). High-precision and conservative: the body must
// reduce to the title with zero extra words, so any genuine added detail (steps, location, expected vs actual)
// clears it. Distinct from the unfilled-template signal, whose body has no real word at all. (#533)
export function buildTitleRestatementIssueFinding(input: IssueSlopAssessmentInput): SignalFinding | null {
const title = normalizeIssueText(input.title ?? "");
const body = normalizeIssueText(input.body ?? "");
// Need both a real title and a real body to compare; an empty side is another signal's concern.
if (title.length === 0 || body.length === 0) return null;
if (body !== title) return null;
// Static, public-safe text (no interpolation) — no sanitizer guard needed.
const detail = "The issue body only restates the title and adds no further detail.";
return {
code: "title_only_restatement",
title: "Issue body only restates the title",
severity: "warning",
detail,
action: "Add detail beyond the title: what is wrong, where it happens, and why it matters.",
publicText: detail,
};
}

function stripHtmlComments(input: string): string {
let output = "";
let cursor = 0;

while (cursor < input.length) {
const commentStart = input.indexOf("<!--", cursor);
if (commentStart === -1) {
output += input.slice(cursor);
break;
}

output += input.slice(cursor, commentStart);
const commentEnd = input.indexOf("-->", commentStart + 4);
if (commentEnd === -1) {
// An unterminated "<!--" is rendered by GitHub/CommonMark as a comment running to end-of-body — the
// text is hidden — so it must NOT survive as substantive content. Dropping it (rather than appending
// it) closes an evasion where a placeholder-only body dodges the unfilled-template signal just by
// omitting the closing "-->". Real prose BEFORE the comment was already appended above and is kept.
break;
}

cursor = commentEnd + 3;
}

return output;
}
Loading