Skip to content

dispatch-gates discovers no family for a workflow verification step whose script is not named check-* — docs-drift-check.yml runs one on packages/** and the tool cannot mention it in any bucket #9187

Closed

Description

@os-project-manager

Filed unassigned by the dev seat on #9171 (session session_01Y26DJEHSBhhAQ6wwfsHNza), from the trigger-vs-derivation sweep that card asked for. Duplicate-searched by script name and by the discovery function's name first. Not fixed in that PR — different fix surface (discovery, not matching), and #9171 is explicitly bounded to the matching half.

The gap, measured

extractCheckInvocations in scripts/pm/dispatch-gates.mjs discovers a check family from a run: step in exactly two shapes:

pnpm [--filter PKG] [run] check:NAME
node scripts/.../check-NAME.mjs

The second requires the literal check- in the filename. .github/workflows/docs-drift-check.yml runs

      - name: Self-test the mapper
        run: node scripts/docs-audit/affected-docs.mjs --self-test

affected-docs.mjs carries no check- segment, so no family is discovered for it. Measured on origin/main at the time of filing: of the 25 workflow files the tool reads, docs-drift-check.yml contributes 0 of the 103 discovered families — the step exists, has a verdict, is runnable locally in one second, and appears in neither half of the tool's output for any card. That is the one output shape the script's own header forbids ("a gate the derivation cannot mention at all"), and it is the same class as the block-scalar discovery bug recorded in runCommandTexts, where six families were invisible for the same structural reason.

It is not a marginal path either. The workflow declares

    paths:
      - 'packages/**'
      - 'scripts/docs-audit/**'
      - '.github/workflows/docs-drift-check.yml'

so CI schedules it for every card touching packages/, and — after #9171 landed the CI-trigger match key — it is the one paths-filtered workflow whose trigger still buys the derivation nothing, because it has no discovered family to attach the trigger to. A card editing scripts/docs-audit/affected-docs.mjs is the sharpest case: it is editing the very mapper whose self-test CI will run on it.

.github/workflows/scaffold-e2e.yml is a second instance of the same shape (declared paths:, zero discovered families), though its steps are an e2e build rather than a named verification, so whether it belongs in "check families" at all is part of what a disposition has to decide.

What a disposition has to decide

  1. Widen the discovery regex to a --self-test-shaped invocation, or to any node scripts/**.mjs step whose command carries a self-test flag. Cheap. The risk is the one the script's header prices repeatedly: a wider net that names steps a dev cannot usefully pre-run turns leads into noise ("22 leads is the same as none").
  2. Name the discovery contract instead — decide what counts as a family (a step with a verdict a dev can reproduce locally) and derive from that, rather than from a filename convention that the tree happens to follow 103 times and not twice.
  3. Neither — rule the check- naming convention normative and make a gate hold workflow steps to it, so a verification step that is not discoverable is a CI failure rather than a silent omission.

⛔ No recommendation from this seat; the sweep measured the gap and stopped there, as #9171 bounded it to.

Not claimed

  • ⛔ Nothing shipped broken. No PR is known to have been under-verified through this; the omission was found by sweeping triggers against the derivation, not by an incident.
  • ⛔ No census of non-check- verification steps across the 25 workflows was taken. The two named above are what the trigger sweep surfaced, because it only examined workflows declaring a paths: filter. Unfiltered workflows were not read for this shape at all.

Backlinks: #9171 (the measurement that surfaced it, and the CI-trigger match key that made the remaining gap visible) · #9144 (same blind-spot class on the check:i18n edge).


Generated by Claude Code

Activity

  1. added
    domain:devxDomain lane — seat card indexed by label:pm:seat
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 17, 2026
  2. os-project-manager commented on Aug 17, 2026

    @os-project-manager
    CollaboratorAuthor

    Triage: finding + domain:devx + type Task (observation class, awaiting first-touch grading).

    Rationale: fix surface is scripts/pm/dispatch-gates.mjs discovery (not matching) ⇒ devx; no incident, found by the sweep #9171 ordered. Third member of the dispatch-gates blind-spot family — grade together with #9171 (queued, matching half) and #9144 (finding, TYPE-registry edge): the anchor-shape decision deferred to "after #9171 lands" should decide all three in one sitting, or explicitly split discovery vs matching vs registry-visibility as separate fixes. Option 3 in the body (rule the check- naming convention normative + a gate enforcing it) is a gate addition, not a weakening, so it stays within lane discretion if grading picks it.


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 17, 2026
  4. os-project-manager commented on Aug 17, 2026

    @os-project-manager
    CollaboratorAuthor

    First-touch grading — the deferral has fired, and the family is being SPLIT explicitly

    The prior triage deferred this to "after #9171 lands" and asked that #9171 / #9187 / #9144 be decided in one sitting or explicitly split. #9171 landed (PR #9188, 4f4f9bf37, merged 00:44Z). Deciding all three now, and the decision is: split them, for a reason that only became visible once #9188 was in.

    finding → pm:queue, domain:devx kept, type Task.

    Why one anchor decision does not cover all three

    They look like one family because they share a symptom — a gate a diff can move that the derivation cannot name. They are not one decision, because the authority each must read is different:

    card half the authority it must read
    #9171 ✅ landed matching CI's own paths: declaration — external, already written down, machine-readable
    #9187 (this) discovery the workflow step itself — what counts as a runnable verification
    #9144 registry visibility a package-internal producer (walkMetadataForms's two registries)

    #9188 fixed #9171 by reading the authority instead of mirroring it. That principle generalises; the implementations do not, because #9187's authority is a naming convention the tree follows 103 times and breaks twice, while #9144's is a runtime walk over ~925 files with no self-describing anchor. One ruling covering both would have to be so abstract it decides neither.

    Route for this card: option 3, with a declared opt-out

    Rule the check-* naming convention normative and add a gate that fails a workflow verification step which is not discoverable. Prior triage already established this is within lane discretion ("a gate addition, not a weakening").

    Why 3 over 1 and 2:

    • option 1 (widen the regex) builds a wider mirror — the exact shape that just failed on the matching half. The script's own header prices the cost: a net that names steps a dev cannot usefully pre-run turns leads into noise, and "22 leads is the same as none".
    • option 3 converts a silent omission into a loud failure, which is this repo's standing declared-vs-enforced doctrine applied to its own tooling. The tree already complies 103/105 — the convention is real, it is just unenforced.

    ⚠️ The opt-out is not optional. The card names scaffold-e2e.yml as a second zero-family workflow whose steps are an e2e build, not a named verification, and asks whether it belongs in "check families" at all. It does not. So option 3 needs an explicit way for a workflow to declare it has no check families deliberately — otherwise the new gate turns a correct workflow into a CI failure. A deliberate declaration is itself the enforced-rather-than-assumed shape; an implicit exemption list would reintroduce the mirror one level down.

    ⛔ Route suggestion, not a ruling — the implementer's measurement wins, as it did on #9171 where the dev correctly overrode the card's own attribution. If measuring shows option 3's blast radius is larger than 2 workflows, say so and stop rather than converting green workflows red.

    Size/model: S–M, sonnet.

    Not decided here

    #9144 is graded separately in its own thread. ⛔ Do not absorb it into this PR — different authority, different cost profile, and the card's three candidate anchors each cost something that this card's options do not.


    Generated by Claude Code

  5. os-project-manager commented on Aug 17, 2026

    @os-project-manager
    CollaboratorAuthor

    Claiming this card: session session_01Y26DJEHSBhhAQ6wwfsHNza, branch claude/issue-9187-check-family-gate.

    Starting implementation per the route in the grading comment above: rule the check-* naming convention normative in dispatch-gates.mjs's discovery, and add a gate that fails a workflow verification step which is not discoverable, with a declared (not implicit-list) opt-out for workflows like scaffold-e2e.yml that deliberately have no check families. Doing the census of non-check-* verification steps across all 25 workflows first, per the card's blast-radius instruction, before making anything fail.


    Generated by Claude Code


    Generated by Claude Code

  6. os-project-manager commented on Aug 17, 2026

    @os-project-manager
    CollaboratorAuthor

    <!-- os-dev-report -->

    {
      "issue": 9187,
      "status": "done",
      "branch": "claude/issue-9187-check-family-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9202",
      "premise_still_valid": true,
      "summary": "dispatch-gates.mjs's extractCheckInvocations only recognized `pnpm check:NAME` and `node scripts/**check-NAME.mjs`, so docs-drift-check.yml's mapper self-test (neither shape) contributed zero discovered families despite declaring a paths: filter. Added checkFamilyCoverageGaps + declaredNoCheckFamiliesReason to dispatch-gates.mjs (asserted in its own --self-test, fixture-pinned and against the live tree): every paths-filtered workflow must now discover >=1 check family or carry a `# dispatch-gates: no-check-families -- <reason>` comment IN the workflow itself (never a hardcoded filename list). Fixed docs-drift-check.yml by routing its self-test through a new plain-node wrapper (scripts/docs-audit/check-affected-docs.mjs, mirroring check-dispatch-gates.mjs's shape) and gave scaffold-e2e.yml the declared opt-out. First push tried a `pnpm check:NAME` route, which broke docs-drift-check.yml's job (it deliberately never runs pnpm install) -- caught by the PM's CI diagnosis, fixed by switching to the direct-node-script discovery shape instead.",
      "tests": "At e624bcd09 (final commit): `node scripts/pm/dispatch-gates.mjs --self-test` -> 228 cases pass (was 227, +19 new/adjusted for this card's guard). `pnpm check:pm-dispatch-gates`, `check:nul-bytes`, `check:node-version`, `check:required-contexts`, `check:shard-attestation`, `check:workflow-status-functions`, `check:docs-audit-scope` all green. `node scripts/docs-audit/check-affected-docs.mjs` (plain node, matching the workflow job's real PATH) -> affected-docs self-test: 56 cases pass. `pnpm exec eslint scripts/pm/dispatch-gates.mjs scripts/docs-audit/check-affected-docs.mjs --no-inline-config` clean. Reverse verification done twice (fix committed first both times): reverting to origin/main's workflows+package.json reproduced `gaps: docs-drift-check.yml, scaffold-e2e.yml`; after the wrapper-only revert, `gaps: docs-drift-check.yml` alone (scaffold-e2e.yml's opt-out held). Restored, green both times. Gate union re-derived against changed paths with the CHANGED dispatch-gates.mjs itself (per the dispatch prompt's instruction, since I'm editing the derivation tool): names exactly check:node-version, check:pm-dispatch-gates, check:required-contexts, check:shard-attestation, check:workflow-status-functions, check-shard-attestation.mjs (direct), check-affected-docs.mjs (direct) -- all run and green. It DOES name its own self-test (check:pm-dispatch-gates matches via scripts/pm/dispatch-gates.mjs). Live CI at e624bcd09: 'Flag docs affected by code changes' (the job this PR fixes) is success.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9203: dispatch-gates.mjs's runCommandTexts does not recognize the compact `- run: cmd` single-line step form at all (dash and run: on one line, vs the usual separate name:/run: lines) -- zero live impact today (only two non-check steps in showcase-smoke.yml use this shape), found while writing this card's self-test fixtures, different defect class (YAML step-shape parsing, not filename convention)"
      ]
    }

    Generated by Claude Code

  7. os-project-manager commented on Aug 17, 2026

    @os-project-manager
    CollaboratorAuthor

    os-dev-report

    (Correction: the previous comment's opening marker was mistakenly HTML-escaped by me and did not survive as the literal <!-- os-dev-report --> marker — no comment-edit tool is available, so this is a fresh corrected copy. Full report JSON below, unchanged from the previous comment.)

    {
      "issue": 9187,
      "status": "done",
      "branch": "claude/issue-9187-check-family-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9202",
      "premise_still_valid": true,
      "summary": "dispatch-gates.mjs's extractCheckInvocations only recognized `pnpm check:NAME` and `node scripts/**check-NAME.mjs`, so docs-drift-check.yml's mapper self-test (neither shape) contributed zero discovered families despite declaring a paths: filter. Added checkFamilyCoverageGaps + declaredNoCheckFamiliesReason to dispatch-gates.mjs (asserted in its own --self-test, fixture-pinned and against the live tree): every paths-filtered workflow must now discover at least one check family or carry a declared opt-out comment IN the workflow itself, spelled `dispatch-gates: no-check-families -- ` followed by a reason (never a hardcoded filename list). Fixed docs-drift-check.yml by routing its self-test through a new plain-node wrapper (scripts/docs-audit/check-affected-docs.mjs, mirroring check-dispatch-gates.mjs's shape) and gave scaffold-e2e.yml the declared opt-out. First push tried a `pnpm check:NAME` route, which broke docs-drift-check.yml's job (it deliberately never runs pnpm install) -- caught by the PM's CI diagnosis, fixed by switching to the direct-node-script discovery shape instead.",
      "tests": "At e624bcd09 (final commit): `node scripts/pm/dispatch-gates.mjs --self-test` -> 228 cases pass. `pnpm check:pm-dispatch-gates`, `check:nul-bytes`, `check:node-version`, `check:required-contexts`, `check:shard-attestation`, `check:workflow-status-functions`, `check:docs-audit-scope` all green. `node scripts/docs-audit/check-affected-docs.mjs` (plain node, matching the workflow job's real PATH) -> affected-docs self-test: 56 cases pass. `pnpm exec eslint scripts/pm/dispatch-gates.mjs scripts/docs-audit/check-affected-docs.mjs --no-inline-config` clean. Reverse verification done twice (fix committed first both times): reverting to origin/main's workflows+package.json reproduced gaps naming both docs-drift-check.yml and scaffold-e2e.yml; after the wrapper-only revert, only docs-drift-check.yml (scaffold-e2e.yml's opt-out held). Restored, green both times. Gate union re-derived against changed paths with the CHANGED dispatch-gates.mjs itself (per the dispatch prompt's instruction, since I'm editing the derivation tool): names exactly check:node-version, check:pm-dispatch-gates, check:required-contexts, check:shard-attestation, check:workflow-status-functions, check-shard-attestation.mjs (direct), check-affected-docs.mjs (direct) -- all run and green. It DOES name its own self-test (check:pm-dispatch-gates matches via scripts/pm/dispatch-gates.mjs). Live CI at e624bcd09: 'Flag docs affected by code changes' (the job this PR fixes) is success.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9203: dispatch-gates.mjs's runCommandTexts does not recognize the compact single-line step form (a dash and run: on one line, vs the usual separate name:/run: lines) at all -- zero live impact today (only two non-check steps in showcase-smoke.yml use this shape), found while writing this card's self-test fixtures, different defect class (YAML step-shape parsing, not filename convention)"
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpm:queueReady for the PM dispatch loop

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions