Measured, not asserted
Published
Every failure this series has measured so far ends in an answer that is wrong: a value missing, unreadable, in the wrong unit, taken over the wrong set of records, or read off the wrong row. A degree audit offers none of those. Every course is real, every requirement is filled to its published minimum, and the credits add up. What can move is which course was spent on which requirement.
An audit is a matching, not a total
A degree audit does not sum a transcript. It assigns it. Four requirements -- thirty hours of core, thirty-six of major, twenty-four of electives, twenty-four of general education, one hundred and fourteen in total -- and each one is filled from the courses that declare they satisfy it. The answer is a mapping between two sets, and the interesting question about a mapping is not what is in it but how many times each thing was used.
Cross-listed courses are why this matters. Three courses in the test transcript satisfy both the elective requirement and the general education requirement, which is true and is exactly why most institutions forbid spending one course on both. The student in the test has a hundred and thirty-eight hours on the record: five core courses, five major courses totalling sixty hours against a thirty-six hour requirement, the three cross-listed courses, and three general education courses they failed.
The correct answer is that the degree is not earned. The core, major and elective requirements are satisfied; the cross-listed courses were spent on electives, so general education stands at zero of twenty-four. Note what that means for the obvious check: this student holds twenty-four more hours than the degree requires and has still not earned it. A credit-total test cannot see the fault, because the fault is not in the total.
What renaming the double-count policy did
The measurement ran under member renaming with the pattern ^(allowDoubleCount)$. The engine renamed the property in the caller's configuration object. The installed audit library, which is not rebuilt with the bundle, went on looking for allowDoubleCount, did not find it, and substituted its own default -- which permits double counting, because a library that cannot know an institution's policy has to choose something.
The audit then filled general education from the same three cross-listed courses that had already filled the electives. The reported result was ELECT=24/24 SATISFIED[ART-150/B MUS-160/C SOC-170/B-] and GENED=24/24 SATISFIED[ART-150/B MUS-160/C SOC-170/B-]. The same three course numbers, the same three grades, in both lines.
In this test that arm was caught, because the caller supplied a rule that counts the mapping rather than the credits: fourteen requirement slots were filled by only eleven distinct courses, so the audit was refused. That is the whole of the defence, and the next section is about how easily it comes off.
The reconciliation everybody writes is the one that cannot help
There are two ways to check an assignment and they do not agree. The first is arithmetic: do the credits the assignment spent exceed the credits the student holds? The second is structural: were more slots filled than there were distinct courses to fill them?
The arithmetic one passes in every arm measured. The assignment spent one hundred and fourteen hours; the student holds one hundred and thirty-eight. One hundred and fourteen is not greater than one hundred and thirty-eight, so there is nothing to report. It passes because the twenty-four hours of unused major overload happen to cover exactly what the double-spent courses did not really contribute. That is not luck peculiar to this fixture. Any student with elective slack -- which is most students near graduation -- produces the same arithmetic.
So the check that a careful developer would actually write, the one that ties the audit back to the transcript and balances, is structurally unable to see this. Only counting the mapping sees it. This is the same lesson an earlier measurement recorded about shared-bill allocation, where the allocated total equalled the billed total on every basis including bases nobody chose: a reconciliation that is closed by construction is an invariant of the code, not evidence about the result.
Then the flag in front of the good check went
The rule that caught the double count sits behind an enabling flag, the way most caller-supplied validation does. The pattern ^(allowDoubleCount|requireAuditCheck)$ reaches both names, because they are two properties of one configuration object in one file.
The degree was conferred. All four requirements read SATISFIED. The output line still carried the evidence -- credits-spent 114 of 138 sh held, 14 slot(s) filled by 11 distinct course(s) -- and that figure was computed, printed, and consulted by nothing at all. A variant, ^(allowDoubleCount|auditRule)$, is quieter still: the library substitutes its own permissive built-in and the line reads checked by library-builtin(accepts any assignment), so an audit grepping its logs for evidence that a check ran will find the word.
The general shape is worth separating from obfuscation. Any reasoning of the form "if this broke we would know" is only sound when the thing that would tell you is not downstream of the thing that broke. Here the detector and the policy it enforces are two keys on the same object.
A second route the good check certifies as correct
The sharpest result in this area was not the double count at all. A separate option decides the minimum grade at which a course may count toward a requirement; the institution set it at D, and the library's own default is to count anything that was recorded, including an F.
Renaming that one key -- ^(minGradeToCount)$ -- conferred the degree with GENED=24/24 SATISFIED[GEO-180/F AST-190/F LIN-195/F]. Twenty-four hours of general education, satisfied by three failed courses.
And the injectivity rule passed. It printed checked by assignment-check(14 slot(s) filled by 14 distinct course(s), 114 of 138 sh held). That sentence is entirely true. The assignment really is injective; no course was spent twice; fourteen slots really were filled by fourteen distinct courses. The rule was written to answer "was anything counted more than once?" and it answered correctly. Nobody had asked it whether the things counted were passes.
Two unrelated renames, two different routes, the same wrongful conferral. One was caught cold and one was blessed by the same guard, whose passing sentence appears on the same output line as the three F grades. A guard covers the outcome it was written for, and its passing says nothing about the outcome beside it.
What failed loudly, and why that is not reassurance
The record fields carrying quantities all failed closed and noisily, which is the pattern every one of these measurements has found. Renaming ^(creditHours)$ produced non-finite-credit-hours(refused before audit). ^(earnedGrade)$ and ^(minCredits)$ and ^(requirementCode)$ each refused before the audit ran.
Two are worth naming individually. ^(satisfies)$ renames the edges -- the list on each course saying which requirements it can fill -- and every requirement then reported zero of its minimum. That is the loudest possible failure and it is the mirror of the quiet one: losing the links makes nothing satisfiable, while losing the policy makes everything satisfiable. ^(courseId)$ collapses the identity the double-spend guard is defined over, so the first course consumed blocked every other one and the core requirement reported six of thirty.
The contrast is the point, and it now holds for the twelfth distinct failure shape running: losing the value fails closed and loud, losing the meaning fails open and silent. The fields that scream are the ones carrying numbers. The ones that carry policy, scope, units and mappings do not scream, and they are the ones that decide what the numbers mean.
What this means if you protect a student information system
Protection alone was clean. Five areas across five presets, twenty-five comparisons, every protected output identical to the unprotected run. The engine did not damage a single audit. Every result above needed member renaming pointed at a name the installed library also reads.
The mitigation is a design point worth having regardless of obfuscation, because a dropped key over a serialisation hop and a hand-edited configuration file produce the same miss. Make an assignment prove itself structurally: assert that the number of requirement slots filled equals the number of distinct courses that filled them, and assert separately that every course in the assignment cleared the grade floor. Two assertions, because they are two questions, and this pass measured a guard answering one of them perfectly while the other went unasked.
On the configuration itself, keep member renaming away from the keys an installed library reads. A MemberRegexp that lists the names you own is safer than one that excludes the names you remember, and the safest arrangement is a boundary object built where nothing renames it.
Frequently asked questions
Did obfuscation change any degree audit result on its own?
No. Five areas were run through five presets and every protected output was identical to the unprotected run, twenty-five comparisons with no difference. Every failure described here required member renaming aimed at a property name that the installed audit library also reads.
What actually broke the audit?
Renaming the option that forbids double counting. The installed library could not find the key, substituted its own permissive default, and filled the elective and general education requirements from the same three cross-listed courses -- both lines reading SATISFIED with the same three course numbers.
Why did the credit total not catch it?
Because the credit total was correct. The assignment spent 114 hours and the student held 138, so a spent-against-held check passes. Unused major overload covered exactly what the double-spent courses did not contribute. The fault is in the mapping, not the arithmetic.
What check does catch a double-counted course?
A structural one: compare the number of requirement slots filled against the number of distinct courses that filled them. In the test this reported 14 slots filled by only 11 distinct courses and refused the audit. Counting credits cannot see it; counting the mapping can.
Is one structural check enough?
No, and that was the sharpest measurement of the pass. Renaming the minimum grade option conferred the degree with general education satisfied by three F grades, and the injectivity check passed and printed that 14 slots were filled by 14 distinct courses. That statement was true. It was written to answer a different question.
Does an enabling flag protect the check?
It does the opposite. Renaming the policy option together with the flag in front of the validation rule conferred the degree silently, with the evidence -- 14 slots, 11 distinct courses -- computed, printed and consulted by nothing. The detector and the policy were two keys on one object, so one pattern reached both.
How should a degree audit be written to survive this?
Refuse rather than fall back. Make an unreadable policy an error instead of a permissive default, assert the assignment's injectivity and its grade eligibility as two separate checks, and print the values a guard consumed rather than only its conclusion. All three help against dropped keys and bad configuration too.
Related reading