⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
wantedPathCandidate in src/services/contributor-issue-draft.ts builds one candidate whose sections object contains two fields derived from the same manifest.testExpectations array, handled differently.
implementationRequirements — :486:
...(manifest.testExpectations.length > 0 ? manifest.testExpectations.map((entry) => `Run ${entry} before requesting review.`) : []),
testingRequirements, four lines later in the same literal, calls buildContributorIssueDraftTestingRequirements(manifest) — :127-128:
const policyExpectations = manifest.testExpectations.filter(isFocusManifestPublicSafe).map(formatContributorIssueDraftTestExpectation);
Two mechanical divergences follow:
- No
isFocusManifestPublicSafe filter. A single unsafe testExpectation in a repo's .loopover.yml is embedded in implementationRequirements, and the whole-body check at :281 then flips the candidate to skipped_unsafe — the entire wanted-path issue is dropped instead of the one offending line, which is precisely what the filter at :128 exists to prevent.
- No
formatContributorIssueDraftTestExpectation. An expectation already phrased as a command produces doubled text: "Run npm run test:ci" becomes "Run Run npm run test:ci before requesting review." The formatter's guard at :139-141 exists specifically to avoid this, and testingRequirements in the same object gets it right.
Requirements
src/services/contributor-issue-draft.ts:486 filters through isFocusManifestPublicSafe and maps through formatContributorIssueDraftTestExpectation, exactly as :128 does. Both helpers are already in this module.
- Because the two fields would then compute the identical list, extract that computation into one private helper in this file (e.g.
publicSafeTestExpectations(manifest): string[]) used by both buildContributorIssueDraftTestingRequirements and wantedPathCandidate, so a third consumer cannot repeat the divergence.
buildContributorIssueDraftTestingRequirements's GENERIC_TESTING_REQUIREMENTS fallback when the filtered list is empty (:129) is unchanged and applies only to testingRequirements — implementationRequirements keeps its existing "contribute nothing when the list is empty" behaviour (:486's : [] arm).
- No change to
isFocusManifestPublicSafe, formatContributorIssueDraftTestExpectation, or isContributorIssueDraftPublicSafe.
⚠️ Required pattern: buildContributorIssueDraftTestingRequirements at src/services/contributor-issue-draft.ts:127-133 is the correct handling to mirror, and the shared private helper is what stops the two paths re-diverging. It does NOT satisfy this issue to copy the .filter(...).map(...) chain inline at :486, leaving two hand-written copies; to fix only the doubled-"Run" formatting and leave the missing public-safe filter; or to change isContributorIssueDraftPublicSafe to be less strict about the whole body.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the filter without the formatter, so the doubled-"Run" text survives — does not resolve this issue.
Test Coverage Requirements
99%+ Codecov patch coverage, branch-counted, on src/**; src/services/contributor-issue-draft.ts is inside coverage.include. Both arms of the formatter's guard (:139-141) and both arms of the public-safe filter must be exercised from the new shared helper, plus the empty-list fallback arm.
Expected Outcome
A repo whose .loopover.yml carries one unsafe test expectation still gets its wanted-path contributor issue drafted, with only the offending line removed. Expectations already phrased as commands render once, identically in both sections of the drafted issue.
Links & Resources
src/services/contributor-issue-draft.ts:120-142, :275-295, :470-500; src/signals/focus-manifest.ts.
Context
wantedPathCandidateinsrc/services/contributor-issue-draft.tsbuilds one candidate whosesectionsobject contains two fields derived from the samemanifest.testExpectationsarray, handled differently.implementationRequirements—:486:testingRequirements, four lines later in the same literal, callsbuildContributorIssueDraftTestingRequirements(manifest)—:127-128:Two mechanical divergences follow:
isFocusManifestPublicSafefilter. A single unsafetestExpectationin a repo's.loopover.ymlis embedded inimplementationRequirements, and the whole-body check at:281then flips the candidate toskipped_unsafe— the entire wanted-path issue is dropped instead of the one offending line, which is precisely what the filter at:128exists to prevent.formatContributorIssueDraftTestExpectation. An expectation already phrased as a command produces doubled text:"Run npm run test:ci"becomes"Run Run npm run test:ci before requesting review."The formatter's guard at:139-141exists specifically to avoid this, andtestingRequirementsin the same object gets it right.Requirements
src/services/contributor-issue-draft.ts:486filters throughisFocusManifestPublicSafeand maps throughformatContributorIssueDraftTestExpectation, exactly as:128does. Both helpers are already in this module.publicSafeTestExpectations(manifest): string[]) used by bothbuildContributorIssueDraftTestingRequirementsandwantedPathCandidate, so a third consumer cannot repeat the divergence.buildContributorIssueDraftTestingRequirements'sGENERIC_TESTING_REQUIREMENTSfallback when the filtered list is empty (:129) is unchanged and applies only totestingRequirements—implementationRequirementskeeps its existing "contribute nothing when the list is empty" behaviour (:486's: []arm).isFocusManifestPublicSafe,formatContributorIssueDraftTestExpectation, orisContributorIssueDraftPublicSafe.Deliverables
src/services/contributor-issue-draft.tsproduces the public-safe, formatted expectation list, andmanifest.testExpectationsis not read anywhere else in the file (grep-verifiable).testExpectationsincludes one unsafe entry and one safe entry yields a wanted-path candidate whosestatusis notskipped_unsafe, and whoseimplementationRequirementscontains the safe entry and not the unsafe one.testExpectations: ["Run npm run test:ci"]yields animplementationRequirementsentry equal to"Run npm run test:ci"— no"Run Run"— and atestingRequirementsentry with the identical string.testExpectations: ["npm run test:ci"](no leading verb) yields"Run npm run test:ci before requesting review."in both fields — the formatter's other arm.testExpectationsstill yieldsGENERIC_TESTING_REQUIREMENTSintestingRequirementsand no expectation lines inimplementationRequirements.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the filter without the formatter, so the doubled-"Run" text survives — does not resolve this issue.
Test Coverage Requirements
99%+ Codecov patch coverage, branch-counted, on
src/**;src/services/contributor-issue-draft.tsis insidecoverage.include. Both arms of the formatter's guard (:139-141) and both arms of the public-safe filter must be exercised from the new shared helper, plus the empty-list fallback arm.Expected Outcome
A repo whose
.loopover.ymlcarries one unsafe test expectation still gets its wanted-path contributor issue drafted, with only the offending line removed. Expectations already phrased as commands render once, identically in both sections of the drafted issue.Links & Resources
src/services/contributor-issue-draft.ts:120-142,:275-295,:470-500;src/signals/focus-manifest.ts.