Measured, not asserted
Published
A prerequisite gate walks down a chain: the course needs another course, which needs another, each at a minimum grade. The gate's answer is a list of what is unmet. An empty list means the student is eligible -- and an empty list is also what you get when there is nothing to walk.
Two students look identical to a gate that cannot read the graph
The catalogue in the test requires a fourth-year computing course to sit on two prerequisites: a third-year course at C or better, and a statistics course at C or better. The third-year course needs a second-year course, which needs first-year mathematics, all at C or better. Four levels.
The student is not eligible, on two separate counts. Statistics is in progress this term and carries only a provisional midterm grade, and this institution does not accept a prerequisite taken alongside. Four levels down, mathematics was passed with a D, below the C the catalogue requires, and a waiver once entered against it was withdrawn by the department, so waivers are no longer honoured at all.
Run unprotected, the gate blocks and says why: unmet [STA-220(in progress) MTH-140(D below required C, waiver on file not honored)]. That is the answer every protected run at every preset reproduced.
What renaming the prerequisite links did
The links themselves live in a field on each catalogue record. Renaming ^(prereqs)$ left the installed gate unable to read any of them, so the walk began at the wanted course, found no prerequisites, and stopped.
The student was enrolled. The output reads unmet [], examined 0 prerequisite(s), frontier-open=false, and -- this is the part worth sitting with -- checked by walk-check(examined 0 prerequisite(s), none left).
That sentence is true. The walk did examine every prerequisite it could see, and there genuinely were none left. The completeness assertion is defined over the traversal's own state, and the evidence it reads was produced by the same lost name. A student with a D in first-year mathematics is registered for a fourth-year course, and the gate's own log records a complete and successful check.
A depth limit leaves an edge; the edges leave nothing
The same area has an option controlling how deep the gate verifies. Renaming ^(checkDepth)$ reverted it to the library's default of one level, and that arm was caught: the walk stopped with courses still queued, so the frontier was open, and the caller's rule refused with walk-rejected(walk-check(the walk stopped with prerequisites unexamined)).
Both arms make the walk stop short. Only one of them looks like stopping short. A depth limit leaves a visible edge -- work queued and not done -- which is exactly what a completeness check is able to notice. Losing the links leaves no edge at all, because a graph with no readable links is exhausted at depth zero and there was never anything to queue.
This is the same result an earlier measurement found walking a batch genealogy for a product recall, and the polarity has flipped in the dangerous direction. There, an unreadable graph under-reported a recall: the answer was a correct prefix of the right answer and every row in it survived inspection. Here an unreadable graph produces the permissive answer, because an empty list of problems is indistinguishable from no problems.
Two independent controls that fell together
The two reasons this student is ineligible are enforced by two deliberately separate controls: one refusing a prerequisite taken concurrently, one refusing to honour waivers. That is defence in depth, and each was measured on its own first.
^(allowConcurrent)$ alone: still blocked, unmet [MTH-140(D below required C, waiver on file not honored)]. ^(honorWaivers)$ alone: still blocked, unmet [STA-220(in progress)]. Each half changed only which reason was listed. That is precisely the finding a reviewer closes as cosmetic -- a diff in a log string, no behaviour change, the other layer caught it.
^(allowConcurrent|honorWaivers)$ together: enrolled, unmet [], after a genuinely complete walk -- examined 4 prerequisite(s) over 4 of 4 level(s). This is not the empty-graph artefact. The walk saw all four prerequisites and cleared each of them, because each was blocked by exactly one of the two controls that were lost.
Redundancy is not independence when one pattern reaches both layers. The two controls were two properties of one options object in one file, so a single regular expression covered both. A second layer only survives if it lives where the transformation cannot reach it -- which for an enrolment gate means re-checking on the server, not adding another flag beside the first. A record-field route reached the same result: ^(inProgress|honorWaivers)$ also enrolled the student.
What failed loudly
The grade comparisons failed closed, and their reasons were accurate. ^(minGrade)$ made the required grade unreadable and the gate reported CSC-320(required grade undefined not on scale) rather than guessing. ^(completedGrade)$ did the same from the other side. ^(courseId)$ refused before the walk began with unidentified-course.
Refusing to compare against a grade it cannot read is the correct behaviour, and it is worth noticing that the engine had a defensible alternative available and did not take it. It could have treated an unreadable minimum as no minimum. Libraries that make that choice are where the quiet failures come from.
What this means if you protect course registration
Protection alone was clean: twenty-five comparisons across five presets with every protected output identical to the unprotected run. The gate blocked correctly in every one. Everything above required member renaming pointed at names the installed gate also reads.
The mitigation is the one an earlier pass arrived at and this pass sharpened. Compare against an independent expectation, never against the traversal's own state. The catalogue says how many prerequisites a course has; assert that the walk examined that many. A gate that verified zero prerequisites for a course the catalogue says has two should refuse, and no check written in terms of the walk's own frontier can tell you that.
The second mitigation is structural. Do not enforce an enrolment rule only in code that ships to a browser, and do not count two flags on one configuration object as two layers. Re-scope the decision server-side, where a client-side rename cannot reach either half of it.
Frequently asked questions
Did obfuscation change any enrolment decision on its own?
No. Five areas at five presets gave twenty-five comparisons with no differences; the gate blocked correctly in every protected run. Each failure below required member renaming aimed at a name the installed prerequisite engine also reads.
What enrolled an ineligible student?
Renaming the field that holds the prerequisite links. With no readable edges the walk found nothing to examine, reported an empty unmet list, and the enrolment was permitted -- for a student with a D in first-year mathematics four levels down the chain.
Why did the completeness check not catch it?
Because it was true. The check asks whether the walk stopped with work outstanding, and the walk had exhausted every prerequisite it could see. There were none. A completeness assertion defined over a traversal's own state reads evidence produced by the same lost name.
Was any truncated walk caught?
Yes. Renaming the depth option reverted it to one level, which left courses queued and unexamined, and the caller's rule refused the walk. A depth limit leaves a visible edge; losing the links leaves nothing to notice, because the graph is exhausted at depth zero.
What happened with two independent controls?
They fell together. Renaming the concurrency rule alone and the waiver rule alone each left the enrolment blocked and changed only which reason was logged. Renaming both permitted the enrolment with an empty unmet list, after a complete four-level walk.
Does adding another validation flag help?
Not if a single pattern reaches both. The two controls here were two properties of one options object in one file. A second layer survives only where the transformation cannot reach it, which in practice means re-checking eligibility on the server.
What should a prerequisite gate assert?
An independent expectation. The catalogue knows how many prerequisites a course carries, so assert that the walk examined that many and refuse when it did not. That also protects against a dropped link column or a serialisation hop, which produce the same empty walk.
Related reading