Sub of #17922 · sibling of #17924, #17964, #17968 · covers the fix shipped by #16289
Context
c623b2f63c (#17791 via #17806) deleted 804 unit spec files on the test/playwright/unit/ai/** path prefix. #17922 censused the 30 under ai/buildScripts/ and found 27 whose subject never left this repository. Twenty-one recover verbatim and landed in #17924 / PR #17925. Six were tabled with a named cause each.
Two of those six are already delivered, measured at dev@40edf8abfa:
| tabled residual |
actual state |
AC-4, the check-spec-retirement narrowing |
DONE — f189cc6ffa (#17964, CLOSED) shipped deriveSubjectSuffix, findSurvivingSubjects, unaccountedSurvivors |
DataSyncPipeline.spec.mjs |
RESTORED — f809d86e29 (#17920 / #17927), now at test/playwright/unit/buildScripts/dataSyncPipeline.spec.mjs |
So #17922's owed set is 5, not 6. This ticket takes the first of the five.
The Problem
util/WorkflowScopeClassifier.spec.mjs (237 lines) covered the jobs.changes scope step in .github/workflows/test.yml. That step is not a module — it is an inline actions/github-script body — and it runs on every pull request and decides whether unit and components execute at all.
neomjs/neo CI is exactly those two projects. So a classifier that answers false too eagerly does not turn anything red: it produces a green run with nothing behind it. That is the same failure #17922 exists about, one layer above the specs that ticket restores — and the classifier has had zero coverage since 2026-08-27.
What is uncovered right now, all read from the live script at dev@40edf8abfa:
| behavior |
consequence if it silently breaks |
isCurrentPullRequestHead stale-head guard |
a superseded attempt classifies against a diff that is not the head's |
fail-toward-running on an undeterminable package.json diff (fetch / parse / missing base ref) |
the optimization starts skipping suites it cannot reason about |
fork-PR head content resolved from pull_request.head.repo |
every fork PR fails open into full suites forever — this is precisely the fix #16289 shipped, and it has been unguarded since the sweep |
requiresUnitForContent (resources/content/release-notes/, .../discussions/, apps/portal/resources/data/, learn/) |
the corpus gate is skipped on exactly the PRs it exists to catch |
isComponentsRelevantPath including .github/workflows/test.yml |
a scoping regression ships on the one PR guaranteed not to exercise it |
Note the third row: #16289 is CLOSED COMPLETED, its fix is live, and the only spec asserting it was deleted five days later by an unrelated commit. The fix is correct today by inspection and by nothing else.
Why this one is a rewrite, not a restoration
#17922 tables this spec as "expects run_integration / run_components admissions the classifier no longer makes post-split". Confirmed mechanically rather than inherited:
$ node -e "…yaml.load('.github/workflows/test.yml').jobs.changes.outputs"
{ run_unit, run_components, skip_reason }run_integration and run_parity are gone — both suites left with the extraction. Seven of the nine deleted arms assert them, so a verbatim restore reds on seven arms for a reason that has nothing to do with the classifier being wrong. That is a legitimate topology change in the subject, and dropping those keys is re-aiming, not trimming — which is why AC-3 below requires it to be stated in the file rather than done quietly.
The harness itself — reading the committed script string out of the YAML and evaluating it with AsyncFunction against mocked github / context / core — reconciles cleanly and is what makes the subject testable at all.
The Fix
- Restore to
test/playwright/unit/buildScripts/util/, beside the surviving sibling tree — not the old unit/ai/ path, whose prefix is what classified it as extracted substrate and would re-arm the same deletion at the next boundary change.
- Re-aim the seven arms onto the outputs the job actually declares; name the dropped keys and the reason in the file.
- Add arms for the half the deleted spec never asserted: the stale-head guard and the
run_unit docs/content classification.
- Pin the declared output set, so a suite added back reds here first instead of being silently unasserted.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Behavior |
Fallback |
Docs |
Evidence |
test/playwright/unit/buildScripts/util/WorkflowScopeClassifier.spec.mjs |
#17922 AC-1; sibling precedent check-chore-sync.spec.mjs |
evaluates the committed scope script against mocked Actions globals |
none — a classifier gating all of CI without a spec is the defect |
spec header |
each arm red against a seeded mutation of test.yml |
jobs.changes declared outputs |
.github/workflows/test.yml |
exactly run_unit, run_components, skip_reason |
n/a |
spec header |
first arm asserts the key set |
Acceptance Criteria
Out of Scope
- The other four residuals —
check-theme-surfaces, RebuildContentIndexesAndSeo, PublishReleaseNoteOrphan, WorkflowConcurrency. Each needs its own rewrite-vs-retire judgement; WorkflowConcurrency is the natural next one (same harness, same subject file) and is deliberately left unclaimed.
- Changing what the classifier does. Restoration is not the moment to fix the subject. If an arm reveals a defect, it gets filed, not patched here.
- Making
spec-retirement-lint.yml blocking — #17171 owns required-context binding for the whole *-lint.yml family.
Avoided Traps
- Restoring verbatim to get a fast green. Seven arms would red on outputs that correctly no longer exist, and "fixing" that by deleting assertions without saying so reproduces #17922 in miniature.
- Restoring to the old
ai/ path. That prefix is the deletion trigger.
- Treating a green run as evidence. The whole finding is that green survived the loss of 804 specs; the honest instruments here are the spec-file count and the mutation ledger.
- Asserting
run_integration because the original did. The original was right when it was written; the topology moved under it.
Related
- #17922 — parent; the 27-subject census
- #17924 / PR #17925 — the 21 verbatim restorations
- #17964 — the surviving-subject guard narrowing (delivers #17922 AC-4)
- #16289 — the fork-PR content-resolution fix this spec covers
- #17791 /
c623b2f63c — the sweep that deleted it
Handoff Retrieval Hints
Retrieval Hint: "the CI scope classifier that gates unit and components has no unit spec"
Retrieval Hint: jobs.changes outputs are run_unit/run_components/skip_reason — run_integration and run_parity left with the extraction
Sub of #17922 · sibling of #17924, #17964, #17968 · covers the fix shipped by #16289
Context
c623b2f63c(#17791 via #17806) deleted 804 unit spec files on thetest/playwright/unit/ai/**path prefix. #17922 censused the 30 underai/buildScripts/and found 27 whose subject never left this repository. Twenty-one recover verbatim and landed in #17924 / PR #17925. Six were tabled with a named cause each.Two of those six are already delivered, measured at
dev@40edf8abfa:check-spec-retirementnarrowingf189cc6ffa(#17964, CLOSED) shippedderiveSubjectSuffix,findSurvivingSubjects,unaccountedSurvivorsDataSyncPipeline.spec.mjsf809d86e29(#17920 / #17927), now attest/playwright/unit/buildScripts/dataSyncPipeline.spec.mjsSo #17922's owed set is 5, not 6. This ticket takes the first of the five.
The Problem
util/WorkflowScopeClassifier.spec.mjs(237 lines) covered thejobs.changesscopestep in.github/workflows/test.yml. That step is not a module — it is an inlineactions/github-scriptbody — and it runs on every pull request and decides whetherunitandcomponentsexecute at all.neomjs/neoCI is exactly those two projects. So a classifier that answersfalsetoo eagerly does not turn anything red: it produces a green run with nothing behind it. That is the same failure #17922 exists about, one layer above the specs that ticket restores — and the classifier has had zero coverage since 2026-08-27.What is uncovered right now, all read from the live script at
dev@40edf8abfa:isCurrentPullRequestHeadstale-head guardpackage.jsondiff (fetch / parse / missing base ref)pull_request.head.reporequiresUnitForContent(resources/content/release-notes/,.../discussions/,apps/portal/resources/data/,learn/)isComponentsRelevantPathincluding.github/workflows/test.ymlNote the third row: #16289 is CLOSED COMPLETED, its fix is live, and the only spec asserting it was deleted five days later by an unrelated commit. The fix is correct today by inspection and by nothing else.
Why this one is a rewrite, not a restoration
#17922 tables this spec as "expects
run_integration/run_componentsadmissions the classifier no longer makes post-split". Confirmed mechanically rather than inherited:$ node -e "…yaml.load('.github/workflows/test.yml').jobs.changes.outputs" { run_unit, run_components, skip_reason }run_integrationandrun_parityare gone — both suites left with the extraction. Seven of the nine deleted arms assert them, so a verbatim restore reds on seven arms for a reason that has nothing to do with the classifier being wrong. That is a legitimate topology change in the subject, and dropping those keys is re-aiming, not trimming — which is why AC-3 below requires it to be stated in the file rather than done quietly.The harness itself — reading the committed script string out of the YAML and evaluating it with
AsyncFunctionagainst mockedgithub/context/core— reconciles cleanly and is what makes the subject testable at all.The Fix
test/playwright/unit/buildScripts/util/, beside the surviving sibling tree — not the oldunit/ai/path, whose prefix is what classified it as extracted substrate and would re-arm the same deletion at the next boundary change.run_unitdocs/content classification.Contract Ledger Matrix
test/playwright/unit/buildScripts/util/WorkflowScopeClassifier.spec.mjscheck-chore-sync.spec.mjsscopescript against mocked Actions globalstest.ymljobs.changesdeclared outputs.github/workflows/test.ymlrun_unit,run_components,skip_reasonAcceptance Criteria
WorkflowScopeClassifier.spec.mjsexists undertest/playwright/unit/buildScripts/util/and passes against the livetest.yml.learn/admission, a neutered stale-head guard, and a dropped self-classification.unitproject's collected spec-file count rises by exactly 1, measured by count.Out of Scope
check-theme-surfaces,RebuildContentIndexesAndSeo,PublishReleaseNoteOrphan,WorkflowConcurrency. Each needs its own rewrite-vs-retire judgement;WorkflowConcurrencyis the natural next one (same harness, same subject file) and is deliberately left unclaimed.spec-retirement-lint.ymlblocking — #17171 owns required-context binding for the whole*-lint.ymlfamily.Avoided Traps
ai/path. That prefix is the deletion trigger.run_integrationbecause the original did. The original was right when it was written; the topology moved under it.Related
c623b2f63c— the sweep that deleted itHandoff Retrieval Hints
Retrieval Hint: "the CI scope classifier that gates unit and components has no unit spec"Retrieval Hint: jobs.changes outputs are run_unit/run_components/skip_reason — run_integration and run_parity left with the extraction