KeyNova Sentinel — PMS Phase 1 Defect Report
Date: 2026-08-06
Programme: KeyNova Sentinel — staff-journey certification, Phase 1 (core PMS)
Design: hmp-docs/docs/superpowers/specs/2026-08-06-sentinel-pms-staff-journey-phase-1-design.md
Status of this report: product defects are final. Round results are appended in §4
once the full --suite all --onboard --strict round completes.
1. What Phase 1 set out to do, and what it found
The brief was to make PMS certification answer a question it could not answer: can a real hotel staff member get through their shift? Sentinel could report "133 flows, N passed"; it could not report whether a front-office agent could complete a day, and it had never watched one try — 88 of 133 flows named no browser dimension at all.
Closing that gap surfaced thirteen defects. Six are in the product. Seven are in the certification programme itself, and those matter more than they look: a harness that reports green on nothing is not a neutral absence of testing, it is an active claim of safety.
The single most repeated defect class, found five separate times:
A
404means three different things — no such route, no such resource, and you asked for the wrong path. Every place this programme collapsed them, a regression, a typo and a healthy refusal all became a silent pass.
2. Product defects — ranked
| Rank | Card | P | Defect | Why it ranks here |
|---|---|---|---|---|
| 1 | AB#11250 | P1 | Cash management renders money as ₹NaN — 45 occurrences on /cash-management/drawers, plus Main Safe ₹NaN and Custodian: ₹NaN / ₹NaN limit | A cash screen that cannot state a balance is worse than one showing ₹0: the cashier cannot distinguish "no money" from "we lost the number", and a shift cannot be reconciled against it. Three red-first gates committed. |
| 2 | AB#11252 | P2 | PUT /api/reservations/:id accepts adults/children, returns 200, and silently discards them | The DTO declares both; ReservationsService.update() never maps either. Measured: adults=1 → PUT {adults:3} → 200 whose own body still reads 1. A silently dropped write is worse than a refusal — the caller is told it succeeded. |
| 3 | AB#11253 | P2 | Shortening a stay does not post a STAY_SHORTEN_CREDIT (3 of 4 runs) | The folio keeps nights the guest gave back — the guest is billed for a stay they shortened. Intermittent, which is why it survived. |
| 4 | AB#11251 | P2 | OVERSTAY_NIGHT is taxed but carries no HSN/SAC code | GST Rule 46 requires HSN/SAC per invoice line. Controlled: 0 of 90 overstay lines carry one, while ROOM (13/16) and EXTENSION_NIGHT (7/7) on the same folios do — and the tax resolver already declares OVERSTAY_NIGHT → '996311'. |
| 5 | AB#11248 | P2 | Failing a housekeeping inspection loses the reason, the photo and the re-clean link | inspectionNotes exists and nothing writes it. HousekeepingPhoto.taskId is a required non-nullable FK, so a fail path touching no task cannot anchor evidence — schema-level, not a missing parameter. |
| 6 | AB#11247 | P2 | No walk/relocation workflow exists | The tape chart renders "a guest must be walked" and position-rows.ts:52 deliberately refuses to clamp the negative position because someone must be walked — then offers no verb. ReservationStatus has no WALKED member, so a hand-executed walk lands in pickup history as a cancellation. |
Not filed, deliberately
reference and supplyDate are null across all three night codes. With no
control distinguishing a defect from intended behaviour, that is an observation,
not a finding — recorded as a journey gap. An early draft of the overstay gate
did fail on blank reference; that would have been a false defect.
3. Certification-programme defects
These were all reporting green, or reporting nothing, before Phase 1.
| Card | P | Defect |
|---|---|---|
| AB#11249 | P1 | Nine specs asserted nothing. Not one was a weak assertion — they were harness bugs wearing environment costumes: a missing /api prefix (3 specs), a double-wrapped body, a method that does not exist, a locator matching the wrong control, an assertion encoding a fixed bug, and a hydration race. Each reported as something the environment had failed to provide. |
| AB#11242 | P1 | A pass with no reviewable screen certified green. evidence-integrity.mjs computed blankOnPassing and its own docstring claimed to make it machine-readable — and nothing read it. Review verdicts were not stale, they were unaddressable: keyed by a filename carrying a per-run capture ordinal. 1816 loose PNGs. |
| AB#11245 | P1 | The fixture disabled the control it certifies. checkoutBalanceApprovalThreshold was set to ₹10,00,000, so the manager-approval gate never fired and every scenario meant to certify it passed having never exercised it. |
| AB#11241 | P2 | The anti-weakening gate was dead — failing 4 of 9 on master for an unknown period, so it could not distinguish a real weakening from its own standing failure. It also masked four genuinely malformed records. |
| AB#11240 | P2 | A finding narrated a gap disproven four months before it was written. Both AR receive-payment routes have existed since 2026-03-28; the finding that called them absent probed invented paths. That prose routed an SME review to rule a scenario BLOCKED. |
| AB#11244 | P2 | The round report had no staff-journey lens — renderUseCaseLens existed and was reachable only from --report/--catalogue, never from the report a round writes. |
| AB#11246 | P1 | The staff-journey registry held one entry, under a header saying the steward grows it. |
4. Round results — sentinel-2026-08-05-21-09
node sentinel.mjs --suite all --onboard --strict, SUPER_ADMIN exported before launch.
Fresh tenant 93869d13… + property 397be508… + 138 rooms across 3 room types, plus a
second tenant as the cross-tenant caller. Runtime ~45 min.
The number that matters
15 of 53 staff journeys GREEN · 37 FAIL · 1 INCONCLUSIVE.
This is the first round in the programme's history that could answer the question at all. Before Phase 1 the report could say "133 flows, N passed" and nothing about whether a shift works.
Scorecard
| Flows | 140 · 134 verifiable (96% of the surface) · 132 ran |
| Result | PASS 85 · FAIL 42 · WARN 4 · INFO 1 · SKIP 2 · PENDING 6 · ERROR 0 |
| Pass-rate | 65% |
--strict violations | 16 — of which 11 are POS, out of Phase 1 scope |
Evidence — the pipeline works now
| 2026-08-04 | this round | |
|---|---|---|
| Screens captured | 10 | 82 |
blankOnPassing | 2 | 0 |
| Integrity verdict | ok: false (ignored) | ok: true |
All 18 blank frames were correctly classified apiOnly — a by-design absence, not a
broken capture. The C1 gate did not need to fire, which is the right outcome for its
first live round. Element review is OWED on 82/82 and is the remaining gate before any
scenario may be called GREEN.
⚠ THE 42 FAILURES ARE NOT 42 DEFECTS
The runner's isolated-retry already flipped 5 specs that were red under load and green alone. Re-running a sample of three more BY HAND against the round's own fresh property:
| Spec | In round | Isolated | Verdict |
|---|---|---|---|
assignable-rooms-picker-guard-parity | 0/4 | 4/4 green | load false-red |
discrepancy-resolution | 6/7 | green | load false-red |
company-ledger-aging-conservation | fail | 0/4, reproducible | genuine, needs routing |
So 2 of 3 sampled failures were false, and the runner's retry had missed both. No failure from this round may be routed as a product defect until it has been re-run isolated. That is the programme's own documented discipline and it held again here.
A finding about MY OWN new spec
FD-OVERSTAY-BILLED passes on the fresh property and is RED on the canary. A fresh
property has no overstays, so the spec takes its empty-population branch and asserts
counts.overstays === 0 — honest, and it proves nothing. AB#11251 is invisible on a
fresh property. The gate needs a seeded overstay to be meaningful in an --onboard
round; until then its green there is vacuous-by-data, not by assertion.
Element review — 6 of 82 screens reviewed · 3 issues (a 50% hit rate)
The review found something on the FIRST screen that no assertion caught, which is the whole argument for the practice.
| Screen | Verdict | Finding |
|---|---|---|
| §269ST settle dialog | ISSUE (AB#11254) | Arithmetic clean — total ₹2,10,263 = paid ₹1,99,999 + remaining ₹10,264 exactly. But the case is titled "hard blocks a ₹2,00,000 cash tender (red panel)" and the captured screen shows 199,999 — one rupee UNDER the ceiling — with no red panel. The screen evidences the sub-ceiling CONTROL, not the refusal. The assertion may be right; the EVIDENCE does not show it. |
| Folio ledger conservation | PASS | Arithmetic verified line by line, not eyeballed: room ₹5,000 → ₹250 (5%, ≤7,500 slab); room ₹15,000 → ₹2,700 (18%, >7,500); running balance 5,250 → 6,090 → 23,790 reconciles at each step; header "incl. ₹2,990 tax" equals the sum EXACTLY. SAC on every line. RES is a confirmation number, no UUID leak. |
| Advance over-tender | PASS | payments 11,120 − charges 10,620 = ₹500, and the negative balance renders as "₹500 · Refund owed to guest" — labelled and signed, not the bare ₹−0 of the AB#10151 class. |
| Folio identifier leak | PASS | No raw UUID anywhere — folio number 8-E2E, reservation KNSNGKY7OT-2026-000007. Room ₹15,000 → ₹2,700 (18%); header tax matches the line exactly. The REF NO column renders an em-dash, not a blank, so the absent charge reference behind AB#11251 is disclosed rather than hidden. |
| Spine · F46 folio-at-booking | ISSUE (AB#11255) | The sharpest finding of the round. assertionsPassed = TRUE, and the captured screen reads "Failed to load folio — Reservation not found". A green assertion on an error screen, in the MANDATORY SPINE, on a money case whose entire claim is "a real folio exists at booking". Either the capture point is wrong, or the case never reads the rendered folio and its browser evidence is decorative. No assertion could have caught this: the test is green, the pipeline healthy, and the frame is not blank so the integrity gate passes it. |
| City ledger | ISSUE (AB#11256 — fixed) | Aging buckets summed to the outstanding balance correctly, but the header rendered "Payment terms: days" — the unit with the number missing, because the Company model has no paymentTerms field. A quieter cousin of ₹NaN: a unit with no figure reads as though a value is present. Also mixed INR 0 with the rupee symbol on one screen. |
76 screens remain. No scenario may be called GREEN until its declared decision screens carry a verdict — that is the evidence bar, and it is deliberately not satisfied yet.
A methodological correction I owe
Wave 2b verified its candidates against the shared canary. This round runs a fresh
onboarded property. Those are different fixture states, and a spec verified on one is
not verified for the other — company-ledger-aging-conservation fails reproducibly on a
fresh property while its L2 sibling was 6/6 green on the canary. De-vacuum verification
must be run against the same fixture mode the round will use.
4a. THE CLASS BEHIND FOUR OF THE SIX PRODUCT DEFECTS
Four of the six were one shape — a reader keyed to a field the writer never sends:
| Card | Client reads | Server has |
|---|---|---|
| AB#11250 | .balance | currentBalance |
| AB#11252 | adults accepted by the DTO | never mapped onto the write |
| AB#11251 | sacCode | absent from the row type entirely |
| AB#11256 | paymentTerms | the field does not exist |
In three of the four the TypeScript type declared the wrong shape — so the compiler validated the defect and would have rejected the fix. A type that does not match its producer stops being a safety net and becomes a guarantee of the wrong thing.
This repo already owns the gate for this class:
src/lib/api/__tests__/response-contracts.test.ts binds each consumer to a verbatim
capture from the real producer and drives the REAL selector against it. Cash,
city-ledger and night-audit surfaces are not registered in it — registering them would
have caught three of the four at compile time. The scoped work is on AB#11250, including
the step that matters: a permanent NEGATIVE capture carrying the pre-fix key, without
which the gate is green by construction.
5. Recommended fix order
- AB#11250 (
₹NaN) — guest-facing money, red-first gates already in place. - AB#11252 (silent pax discard) — a documented API lying about success.
- AB#11253 (shorten credit) — the guest is billed for nights returned.
- AB#11251 (missing SAC) — statutory invoice content; the code is already known to the resolver.
- AB#11248 (inspection fail) — one fix covers reason, photo anchor and re-clean link.
- AB#11247 (walk workflow) — the largest, and the only one that is net-new product work.
The programme defects are already fixed in this phase except where noted; their cards remain open for the record and to carry the regression gates.
6. Method notes worth keeping
- De-vacuum before registering. Of 8 Wave-2b candidates, 4 failed verification and were held. Binding them would have manufactured exactly the fake green the rule exists to prevent.
- One passing run rules in stability no more than one failure rules out
determinism.
stay-shorten-creditpassed once and failed the next three. - A test that encodes a defect becomes a tripwire against its own fix. Two specs went red the moment the product got better, and their red was indistinguishable from a regression.
- A finding without a control is an observation. The SAC defect is a finding because sibling codes on the same folio carry what it lacks.