Defect Recurrence and Boundary Evidence
Tracked by pm-1qkivy, pm-rtn5h6, pm-0pzgit, pm-7c27ep, pm-h8tpeh, and pm-003j.
Agent Quick Context
pm treats project management as context management. A resolved defect is therefore not only a closed item: it is durable context that should select the checks most likely to prevent the same failure from recurring.
The public SDK provides four composable contracts:
- a captured-boundary registry that rejects self-generated fixtures, unsafe samples, missing samples, and expired waivers;
- a defect-evidence ratchet that requires a typed escape class plus a gate improvement or reviewed, expiring waiver on new terminal defects;
- a versioned recurrence index that maps proposed files, packages, PM items, tags, and error codes to shared local and hosted checks;
- a complete-population recurrence coverage report with bounded, resumable evidence rows.
The CLI and MCP use the same SDK action path. Repository policy can be replaced by a package or workspace policy without changing the analyzer.
Public SDK
Import the governance surface from the package root or the narrow governance entrypoint:
import {
analyzeDefectChangeRisk,
buildDefectRecurrenceIndex,
evaluateBoundaryFixtures,
evaluateDefectGateEvidence,
parseDefectRecurrencePolicy,
type DefectChangeRiskInput,
type DefectRecurrencePolicy,
} from "@unbrained/pm-cli/sdk/governance";
const policy: DefectRecurrencePolicy =
parseDefectRecurrencePolicy(serializedPolicy);
const index = buildDefectRecurrenceIndex(policy, pmItems, {
previous_index: previousIndex,
changed_item_ids: changedItemIds,
});
const change: DefectChangeRiskInput = {
files: ["src/sdk/governance/assurance-action.ts"],
item_ids: ["pm-1qkivy"],
};
const report = analyzeDefectChangeRisk(index, change, { limit: 25 });
The index and report are deterministic. policy_fingerprint identifies the versioned policy; index_fingerprint also covers the sparse PM item-to-family contributions. Continuation cursors bind to the latter, so a cursor cannot silently continue against changed context.
Each report explains its exact matching signals, returns deduplicated local and hosted checks, and includes a small cost receipt. Package authors may keep their own recurrence policy and feed the same SDK from a custom command, extension, CI adapter, or application.
CLI and MCP Action
assurance risk accepts one JSON request through the same definition transport already used by SDK and MCP hosts:
risk_request=$(jq -cn \
--slurpfile policy config/defect-recurrence-policy.json \
'{policy:$policy[0],change:{files:["src/sdk/governance/assurance-action.ts"],item_ids:["pm-1qkivy"]},limit:25}')
pm assurance risk --definition "$risk_request" --json
The result uses items as its bounded row collection and publishes .items[] as the stable selector. When next_cursor is present, submit it in the next request. A stale or malformed cursor fails rather than restarting from an ambiguous offset.
Use pm assurance risk for an operator or agent decision. Use the pure SDK functions when a package already owns the item projection, wants to preserve an incremental index, or needs to combine risk with another domain model.
TypeScript compatibility
The exported ASSURANCE_ACTIONS tuple and AssuranceActionResult union include risk, lineages,
DefectChangeRiskReport, and DefectRecurrenceCoverageReport. This is additive at runtime, but TypeScript consumers with an exhaustive action or result switch must add the new branch. The SDK surface snapshot records that source-compatibility change explicitly.
Policy Model
The repository example is config/defect-recurrence-policy.json. Every family declares:
- a stable id, monotonic version, title, and accountable PM item;
- one of
production_defect,nightly_regression,scanner_finding, orreview_caught_late; - file, package, item, tag, or error-code triggers;
- local and hosted checks selected from the same policy;
- a negative-control change that must select the family;
- historical PM items that justify the family;
- maximum escape-rate and false-positive-rate budgets.
Repository policy validation rejects duplicate ids, absent historical examples, missing family negative controls, invalid taxonomy values, and budgets outside zero through one. Deterministic sorting makes the serialized policy merge-friendly.
Register a semantic family seed using its owner, trigger item ids, or historical
item ids. The SDK follows every recorded local recurs_from edge in either
direction to derive family membership for the complete connected lineage.
Cycles and deep chains do not impose a traversal cutoff. Related, parent, and
external edges never establish recurrence coverage; tags and matching file
globs select change risk but cannot certify lineage registration.
Derived members inherit the family's checks. Changes naming a member or one of its exact project-scoped linked files select those checks even when the policy never named that member. Incremental indexes retain sparse direct evidence so removing or changing an edge also removes obsolete inherited matches.
Complete-population coverage
Tracked by pm-7c27ep.
import { analyzeDefectRecurrenceCoverage } from "@unbrained/pm-cli/sdk/governance";
const coverage = analyzeDefectRecurrenceCoverage(policy, allStatusItems, {
limit: 25,
uncoveredOnly: true,
});
Supply an authoritative complete item corpus. The workspace action loads it
with strict read semantics. Unreadable item documents or directories refuse
both workspace analyses and the repository gate with list_source_incomplete.
Embedding hosts can request the same policy with
createAssuranceWorkspaceContext(pmRoot, { strict_read: true }).
Duplicate ids are rejected. Missing local targets
remain in the population and produce missing_recurrence_item; an unregistered
component produces unregistered_recurrence_item. The complete verdict is
computed before filtering or pagination, so a covered first page cannot hide
an uncovered member later in the graph.
lineage_request=$(jq -cn --slurpfile policy config/defect-recurrence-policy.json \
'{policy:$policy[0],limit:25,uncoveredOnly:true}')
pm assurance lineages --definition "$lineage_request" --json
SDK, CLI, and MCP accept the same definition. Use limit from 1 through 100,
cursor, and uncoveredOnly inside that object. Continuations bind the policy,
complete recurrence evidence, item presence, and filter; changing page size is
allowed. total, has_more, and next_cursor describe the selected rows;
population always describes every recorded recurrence member. The CLI analysis
returns a report; the repository gate makes its ok predicate blocking.
The denominator includes covered, uncovered, and missing recurrence ids across
all lifecycle states. Recorded recognized escape_class values produce class
counts; unclassified members are never silently counted as non-escapes.
escape_rate is null for an empty or incompletely classified population.
escape_rate_lower_bound separately reports known production defects divided by
the entire population. Family labels never backfill item classifications, and
budget_scope: "registered_families_only" prevents policy budgets from being
mistaken for proof about an unknown population.
Registration means checks are selected, not that the defect is fixed or a hosted check has passed. The policy includes domain seeds for all current recorded components, while future disconnected recurrences fail the gate until a justified family is registered. Future edges within a registered lineage inherit protection automatically.
Defect Evidence on PM Items
Projects can register the structured fields without changing the SDK:
pm schema add-field escape_class \
--type string \
--commands create,update,list,search,context \
--description "Defect escape taxonomy"
pm schema add-field gate_evidence \
--type object \
--commands create,update,list,search,context \
--description "Gate improvement or explicit waiver evidence"
gate_evidence accepts one of these dispositions:
gate_addedandgate_strengthenedrequiregate_id, a runnablenegative_control, non-emptylocal_checksandhosted_checks, and an accountableowner;explicit_waiverrequires an accountableowner, a concretewaiver_reason, and a futurewaiver_expires_attimestamp.
The evidence epoch lets an adopting project ratchet new closures immediately while backfilling historical items deliberately. A valid completed_at is authoritative, closed_at is the compatibility fallback, and timestamp-less items created after the epoch fail closed; timestamp-less items created before the epoch remain explicitly grandfathered. Reports keep historical escape-class and disposition counts visible even before those older items become closure blockers.
Captured Boundary Fixtures
The repository inventory is config/boundary-fixtures.json. Each externally produced or consumed value must carry either:
- a committed JSON sample with
capture_sourceset tocaptured_redactedorcaptured_verbatim, capture provenance, explicit redactions, input, and observed output; or - an explicit reason, owner, and future expiry for a boundary that cannot yet be captured safely.
The evaluator rejects a self_generated source because a fixture created by the same implementation cannot reveal disagreement with an external format. It also scans committed JSON for common home-directory, package-token, GitHub-token, and private-key patterns.
The Claude Code directory-slug fixture is consumed directly by the author-provenance test. The npm, GitHub Actions, Git commit, and Sentry samples preserve real field shapes while replacing identifiers, paths, URLs, and user data.
Repository Gate
Run the complete local gate after building:
pnpm quality:defect-evidence
node scripts/release/defect-evidence-gate.mjs --negative-control --json
The first command must pass. The negative control must exit 1 after replacing a captured sample with a forbidden source and adding a terminal defect without evidence. The policy-only negative control
adds a disconnected, unregistered recurrence pair and must also exit 1. Focused provider modes are available as --boundary-only, --evidence-only, and --policy-only.
repository-defect-evidence-required is part of the blocking repository-static-quality assurance composition. That makes local and CI behavior share the same provider result, assertion negative control, enforcement, and immutable verdict semantics.
The static quality pipeline also runs node scripts/check-workflow-permissions.mjs.
It parses every GitHub workflow and requires an explicit read-only default token;
write scopes belong on jobs that use them. Run its --negative-control mode to
confirm a workflow-scope contents: write grant exits with status 1. This
protects the least-privilege workflow policy that pm-003j
tracks, even between Scorecard scans.
The mandatory gate-registry check also compares each recurrence family's checks.hosted values with concrete names derived from the repository workflows. It expands static matrix axes and applies exclusions before inclusions according to GitHub's matrix rules. Renamed or removed checks produce named findings. Runtime expressions, unnamed matrix jobs, and static expansions beyond 256 combinations cannot certify a required check name. The public SDK policy parser remains provider-neutral; this workflow binding belongs to the repository gate.
Assurance request schemas declare each family's required identity, triggers, checks, historical IDs, budgets, and negative-control input. Runtime policy validation additionally verifies unique family IDs and that each negative control actually selects its family; structural JSON Schema validation alone cannot prove those relationships. Risk reasons are sorted after direct and inherited matches are combined, using signal, value, then matched evidence as stable ordering keys.
Recovery Producer Census
censusPmRecoveryReferenceProducers scans complete source files for static object-literal recovery fields. It ignores type literals, destructuring patterns, labels, comments, strings, templates, and regular expressions. Aliases such as candidate_commands, nested fallback_candidates[].command, next_best_command, retry_command, and suggested_next_steps normalize to the same six public recovery kinds used by executable reachability verification.
The census fails when a kind has no producer or when a recovery-like envelope field lacks a typed contract. The integration gate scans every src/**/*.ts producer; the existing real-entrypoint corpus then executes or resolves every normalized kind and proves recovery, replacement, or behavior-preserving semantics.
This separation is intentional: the producer census prevents silent omissions as source grows, while entrypoint execution proves that a reference is not merely syntactically present.