Antecedentes parent/child is enforced ONLY in the browser: the action inserts any IDs it is handed, so a child antecedente can be stored without its parent
A child antecedente can be stored WITHOUT its parent, and the browser is the only thing that has ever stopped it. Measured by audit-terra-ca, 2026-08-29, file:line: the parent/child relationship is enforced entirely in client code - antecedentes-tree-core.ts:41-59 does the selection grouping, and :73-80 removes children when the parent is deselected. The server action inserts whatever IDs it is handed, directly (practica-actions.ts:409-419). The database validates identity and the requiresDetail rule only (terra_2_antecedentes_correction_details.sql:74-109); nothing there knows a child has a parent. So a stale tab, a replayed submit, or any future non-form caller stores an orphan child and the insert succeeds. THIS IS THE ONLY RULE IN THE PRACTICA FLOW WHOSE SOLE ENFORCEMENT IS THE BROWSER. Audit checked the whole set: modulo-cirugia coupling has a composite FK, participante roles have a partial unique plus an ENABLE ALWAYS trigger, antecedentes detail has FK/CHECK/trigger, post-op has vocabulary checks inside a REPEATABLE READ transaction plus its own trigger. Two other rules are Node-only (TERRA-129, TERRA-130). This one is client-only, which is a strictly weaker position than either. THE DECISION BEFORE THE FIX, and it is not mine: is the parent a CLINICAL FACT or a disclosure heading? The current form treats it as a selected fact - it writes the parent row - and 17 of the 33 antecedentes exist specifically as subopciones under a parent. If the parent is a fact, an orphan child is an incomplete clinical record and the action must refuse it. If the parent is only a heading, the orphan is harmless and this WI closes as not-a-defect. Do not implement until that is answered, and do not answer it by reading the form - the form is the layer under suspicion. DO NOT CLOSE THIS BY HARDENING THE TREE COMPONENT. Same instruction as TERRA-127: the browser check already works, which is exactly why nobody has ever seen this fail.
Activity
-
CONFIRMED AS A REAL OCCURRENCE, and the premise check that could have killed this WI was run first rather than assumed. db02-terra-cc measured across all 5 live terraPracticas: exactly 2 child-catalog antecedente rows exist, and they DISAGREE. bafe7b34-800c-4268-b90b-823b124b3684 holds "Diabetes - Compensada" WITH "Diabetes" stored in the same practica. 440b8a1d-5f89-48bb-84e2-ed0809382da0 holds "Diabetes - Tipo 2" with NO parent row, createdAt 2026-08-23 12:10:50. WHY THAT DISTINCTION WAS WORTH WAITING FOR. If the app NEVER wrote parent rows, the missing parent would be the design and this WI's premise would be backwards - the defect would be my reading, not the data. It writes the parent sometimes. So the orphan is a genuine divergence in what the same code path produced, not a convention. THE LINK IS BY ID, NOT BY LABEL. antecedentesCatalog 1a2dc02c-24f4-40c7-a503-e9285aba7a82 ("Diabetes - Tipo 2") carries parentId 2832acd2-f716-46ca-bf6a-0c0c62aa7e35, whose own row is label "Diabetes" with parentId NULL - a confirmed root, not a second child. Nothing here rests on the two labels sharing a word. POPULATION IS 5. One occurrence in 5 practicas and in 1 of 2 child selections is a rate, not a rarity, but N=5 is small and that is stated rather than left for a reader to infer a frequency from. STILL BLOCKED ON ELAZAR: whether the parent is a clinical fact that must be stored alongside the child, or a disclosure heading that carries no meaning on its own. The remedy differs - one is a write the action must add, the other means the orphan is correct and this WI closes.
-
QUEUED FOR coder-terra-ca, next after TERRA-124 and TERRA-139 land. Same class as TERRA-136 and as the hole TERRA-130 now covers: a rule that exists only in the browser is not a rule, it is a default. The action inserts whatever antecedente IDs it is handed, so a child antecedente can be stored with no parent by anyone who posts the form directly - and the resulting row is not obviously wrong when read back, which is what makes it expensive to find later. SCOPE, and read it as a boundary rather than a suggestion: enforce the parent requirement in the SERVER ACTION, in the same transaction as the insert. Do not add a DB constraint - that is a separate decision with its own co-tenancy question and it is not yours to take here. Do not fix it in the client alone, which is the state being corrected. Extract the predicate into a dependency-free core module with the effects injected, the way catalog-read-core.ts and access-request-core.ts are, so it can actually be tested under node --test; a server action cannot be, and assertions written against the action would be a file that never executes and looks like coverage. Then run a MUST-FAIL CONTROL before reporting green: break the predicate, watch the named arm go red, restore it. An assertion that has never failed records nothing.
-
Implementation started: server-side parent requirement in the existing transaction; no DB constraint.
-
AUDITED BEFORE PUSH. Approved with one comment correction; the implementation itself is right. WHAT I CHECKED RATHER THAN ACCEPTED. Delegation: practica-actions.ts:290 calls validateAntecedenteParents with the transaction client injected and throws ActionError on refusal, and no copy of the predicate survives in the action - so the four tests exercise the shipped branch, not a parallel implementation. Write-path census: grep for INSERT/UPDATE/DELETE against the antecedentes tables returns exactly one write, practica-actions.ts:453, inside the same transaction and after the guard. So this is a chokepoint today and the guard actually sits on it. Depth: the predicate looks one level up per selected node, but because every ancestor must itself be selected to satisfy its own child, a chain of any depth is covered transitively - a grandchild without its grandparent fails on the parent's own check. Unknown IDs are deliberately left to the FK and the file says so. Must-fail control was run with the sense of the membership test inverted and the named orphan arm went red. 421/421. THE ONE CORRECTION, and it is the recurring class rather than a new one. The comment at :287 says the read goes through THIS transaction so the catalogue CANNOT CHANGE between the guard and the child insert. The transaction does not provide that. Under READ COMMITTED a plain SELECT takes no lock, so another session can soft-delete or re-parent an antecedentesCatalog row between line 290 and line 453 and this transaction will not notice. The sentence asserts a guarantee the mechanism does not give, and nothing fails when it stops being true. In practice antecedentesCatalog is near-static and the exposure is negligible, which is exactly why nobody would ever catch the sentence being wrong. Either soften it to say the read is transaction-bound so it sees the same snapshot the write will use, or take SELECT ... FOR SHARE and let the sentence stand. Do not leave the strong claim over the weak mechanism. NOT IN SCOPE, noted so it is not mistaken for an oversight: a child whose parent has been retired from the catalogue becomes unsavable, because the guard requires the parent ID in the submitted set and the UI will no longer offer it. That is a catalogue-consistency question, not this guard's, and it has no instance today.
-
Shipped main b0ba79ef90ad8fbed4947df669d5acf22e46994a, subject carrying the WI number and the Agent trailer. The parent requirement now lives at the action boundary: practica-actions.ts:290 calls validateAntecedenteParents with the transaction client injected, before any paciente or practica write, and refuses with a Spanish ActionError. Audited before push rather than after - delegation confirmed with no duplicate predicate left in the action, write-path census returned exactly one antecedentes write (:453, same transaction, after the guard), depth covered transitively because every ancestor must itself be selected, must-fail control run with the membership test inverted and the named orphan arm went red. 421/421. One comment corrected pre-push: it had claimed the shared transaction meant the catalogue could not change between guard and insert, which a READ COMMITTED SELECT does not provide. No DB constraint added - that decision stays open and is not this row's.