Process-class finding about the dev playbook, ⛔ not a repo defect and ⛔ not a defect in any gate. ⛔ Ungraded and unrouted — domain:*, priority and type are triage's. Filed unassigned.
Raised by the #15161 dev (PR #15316) after its own round, and filed by the domain:engine PM seat because it is a rule about how devs work rather than something in the tree. ⭐ The dev diagnosed it against itself, having been asked to check whether a gate had missed something; the honest answer was that no gate missed anything.
What happened
PR #15316 changed packages/spec/src/data/field.form.ts and object.form.ts. Two CI reds followed, in packages the diff did not touch:
@objectstack/lint — validate-predicate-path-refs.test.ts carries exact-count pins over the shipped METADATA_FORM_REGISTRY corpus (51 → 53 predicates, 18 → 19 data.type-rooted). It reads that corpus by an ordinary import.
@objectstack/platform-objects — check:i18n reported 7 drifted bundles, because the two new form rows introduce translatable keys the bundles carry.
Both were real, both were the PR's own, and both cost a CI cycle.
The gate did NOT miss them — this is the part worth recording
The obvious suspicion is that check:cross-package-test-inputs should have caught (1), since its name is exactly "your change is an input to another package's tests". It should not have, and it is right not to. Measured by the dev:
- that gate's population is tests whose reads escape their package through the filesystem —
import.meta.url, __dirname, a findUp walk — which is precisely the class turbo's dependency graph cannot see, and which its own header documents as its reason for existing;
- the lint test escapes nothing. It does
import { METADATA_FORM_REGISTRY } from '@objectstack/spec/system', an ordinary import of a declared dependency (packages/lint/package.json lists @objectstack/spec: workspace:*), and carries none of the escaping spellings.
And CI's own affected-set logic worked exactly as designed:
turbo ls --affected (against the merge base) -> 75 packages, and @objectstack/lint is among them
⇒ The gap is in the dev's LOCAL suite selection, and it is a one-line rule
The dev's round-one local suite list was chosen as "the packages I changed" plus "the test files that read the specific file I edited". Neither is the question CI asks. CI asks turbo ls --affected.
⭐ Deriving the local suite list from turbo ls --affected rather than from the changed-package list would have caught BOTH reds — lint and platform-objects are both in the affected 75, and neither was in the round-one list.
Suggested disposition (⛔ not a decision)
A rule in the dev playbook: before claiming a local verification union, derive the affected set with turbo ls --affected against the merge base, and run the suites of every package in it that has one — the changed-package list is not that set.
⚠️ Sequencing note for whoever takes this. The natural home is .claude/agents/os-dev.md, which is (a) a governed surface and (b) carries a clause-① tier mandate — it is a literal entry in MANDATORY_TIER_GLOBS in scripts/pm/dispatch-gates.mjs. So implementing it is a CONTRACT_REVIEW_TIER change on a draft-only, human-merged surface. That does not affect whether the rule is right; it affects who can land it, and it should not be discovered at dispatch time.
⚠️ This card records a rule, ⛔ not a measurement of how often the gap bites. One round with two instances is one round. If the tooling owner wants a frequency reading before spending a governed-surface change, that is a separate and cheap measurement.
Provenance
Process-class finding about the dev playbook, ⛔ not a repo defect and ⛔ not a defect in any gate. ⛔ Ungraded and unrouted —
domain:*, priority and type are triage's. Filed unassigned.Raised by the #15161 dev (PR #15316) after its own round, and filed by the
domain:enginePM seat because it is a rule about how devs work rather than something in the tree. ⭐ The dev diagnosed it against itself, having been asked to check whether a gate had missed something; the honest answer was that no gate missed anything.What happened
PR #15316 changed
packages/spec/src/data/field.form.tsandobject.form.ts. Two CI reds followed, in packages the diff did not touch:@objectstack/lint—validate-predicate-path-refs.test.tscarries exact-count pins over the shippedMETADATA_FORM_REGISTRYcorpus (51 → 53 predicates, 18 → 19data.type-rooted). It reads that corpus by an ordinary import.@objectstack/platform-objects—check:i18nreported 7 drifted bundles, because the two new form rows introduce translatable keys the bundles carry.Both were real, both were the PR's own, and both cost a CI cycle.
The gate did NOT miss them — this is the part worth recording
The obvious suspicion is that
check:cross-package-test-inputsshould have caught (1), since its name is exactly "your change is an input to another package's tests". It should not have, and it is right not to. Measured by the dev:import.meta.url,__dirname, a findUp walk — which is precisely the class turbo's dependency graph cannot see, and which its own header documents as its reason for existing;import { METADATA_FORM_REGISTRY } from '@objectstack/spec/system', an ordinary import of a declared dependency (packages/lint/package.jsonlists@objectstack/spec: workspace:*), and carries none of the escaping spellings.And CI's own affected-set logic worked exactly as designed:
⇒ The gap is in the dev's LOCAL suite selection, and it is a one-line rule
The dev's round-one local suite list was chosen as "the packages I changed" plus "the test files that read the specific file I edited". Neither is the question CI asks. CI asks
turbo ls --affected.⭐ Deriving the local suite list from
turbo ls --affectedrather than from the changed-package list would have caught BOTH reds —lintandplatform-objectsare both in the affected 75, and neither was in the round-one list.Suggested disposition (⛔ not a decision)
A rule in the dev playbook: before claiming a local verification union, derive the affected set with
turbo ls --affectedagainst the merge base, and run the suites of every package in it that has one — the changed-package list is not that set..claude/agents/os-dev.md, which is (a) a governed surface and (b) carries a clause-① tier mandate — it is a literal entry inMANDATORY_TIER_GLOBSinscripts/pm/dispatch-gates.mjs. So implementing it is aCONTRACT_REVIEW_TIERchange on a draft-only, human-merged surface. That does not affect whether the rule is right; it affects who can land it, and it should not be discovered at dispatch time.Provenance
check:cross-package-test-inputswas not modified, and this card does not propose modifying it.