mars
MARS-147
Student duplicate-práctica deletion request: student requests delete (reason=duplicate), picks keeper from pulldown, teacher approves -> non-keeper soft-deleted
Todo normal
cmcoder02-mars-cc
Activity
-
coder02-mars-cc
-
VERIFIED STILL OPEN 2026-09-11. No commit names MARS-147 and there is no student-facing deletion-request flow (no keeper pulldown, no teacher approval step) anywhere under src/app. Related shipped mechanism to build ON rather than around: the soft-delete path at mi-cursada/[id]/actions.ts, and practicaEditLocks for the student edit window. Note the daily-code constraint found during MARS-206/612: formatPracticaCodigo(date, seq) excludes studentUserId, and a mid-sequence soft-delete leaves its number permanently empty because the partial unique index + MAX(seq)+1 only reclaims the HIGHEST slot - so a 'delete the duplicate' feature will visibly gap the student's codes and that should be a stated product decision, not a surprise.
-
Population + scope measurement, live prod reads 2026-09-11 (coder02-mars-cc). Investigation only, nothing changed. DUPLICATE POPULATION, measured two ways over live practicas (deletedAt IS NULL), grouped on studentUserId + reportedPracticeDate + the sorted practicaTeeth.toothNumber set. Loose (that key alone): 61 groups, 124 practicas, 63 removable non-keepers, 46 students. Strict (+ numeroHistoriaClinica): 21 groups, 43 practicas, 22 removable, 20 students. THE GROUPING IS A DEMAND HEURISTIC, NOT A DUPLICATE DETECTOR - the feature is student-declared, so these numbers size demand over the app's whole life and assert nothing about any individual pair. Two practicas on the same date with the same teeth can be legitimate (mars-commons Bug Classification Rule). THE EXISTING DELETE GATE NARROWS THIS SHARPLY. deleteOwnPractica in mi-cursada/[id]/actions.ts already lets a student soft-delete their OWN practica, but ONLY while it is noRevisado, with a full cascade (practicaToothConductos -> practicaTeeth -> practicaImages -> practicas) in one transaction under a single setArchiveContext. So a duplicate whose non-keeper is still ungraded needs no feature at all. Re-split by approval state: strict, 13 of 21 groups are ALL-GRADED (zero noRevisado copies) and 8 of 21 are already self-serviceable today; loose, 46 of 61 all-graded, 15 self-serviceable. MARS-147's real scope is therefore "delete a REVIEWED duplicate", roughly 13-46 groups depending on the key, and the request+approval flow is justified by the graded record, not by deletion itself. CODIGO GAP IS ALREADY DECIDED, NOT AN OPEN QUESTION. formatPracticaCodigo(date, seq) excludes studentUserId and the partial unique index + MAX(seq)+1 only reclaims the HIGHEST slot, so a mid-sequence soft-delete leaves that number permanently empty. The existing noRevisado self-delete already does this, so the decision was made implicitly when that shipped. State it in the design; do not re-open it. PLURAL-APPROVED GROUPS ARE A DESIGN INPUT, NOT A FORK. In 10 of the 21 strict groups more than one copy is already aprobado (27 approved practicas inside those groups). When a student declares an approved copy a duplicate, the teacher-approval step IS the gate - so the approval UI must show both copies' state and grade before the teacher confirms. Deleting a non-keeper changes the student's approved count; that is visible to the approver by construction, which is the remedy. CLASS-A THREE WAYS - needs the pre-implementation design ping before any code: a request record (new DB schema, db-mars-cc authors the DDL), a teacher notification (email-send path), and a soft-delete on four audited tables (needs its own setArchiveContext reason from mars-archive-reasons.md). Approver scope defaults to canViewPractica, which is comision-level; MARS-213 established that Mars has no pick-level gate anywhere, so do not introduce one here.
-
DESIGN, for the pre-implementation ping (Class-A three ways: new schema, email-send path, soft-delete on four audited tables). coder02-mars-cc. Nothing built. BUILD ON THE EXISTING DELETE, DO NOT DUPLICATE IT. noRevisado stays self-service through deleteOwnPractica exactly as today - 8 of 21 strict duplicate groups are already serviceable that way and need no feature. The new flow is ONLY for a práctica that has been reviewed, which is what justifies a request+approval step at all. NEW TABLE practicaDeletionRequests (db-mars-cc authors the DDL), shaped after accessRequests' request/review pattern but with Mars lookup discipline rather than its bare text status: id uuid PK; practicaId uuid FK practicas.id (the NON-KEEPER, the row to delete); keeperPracticaId uuid FK practicas.id (the copy the student keeps); requestedByUserId uuid FK users.id; reasonOptionId uuid FK lookupOptions.id, new category practicaDeletionReason, one value today: duplicate; studentComment text NULL; statusOptionId uuid FK lookupOptions.id, new category practicaDeletionRequestStatus: pending, approved, rejected, cancelled; reviewedByUserId uuid NULL FK users.id; reviewedAt timestamptz NULL; reviewComment text NULL; createdAt, updatedAt; deletedAt timestamptz NULL. ONE OPEN REQUEST PER PRÁCTICA, enforced in the schema not the action: partial unique index on (practicaId) WHERE the status is open, mirroring uq_accessRequests_open_normalized. Server-side validation on submit: both prácticas belong to the requester, both deletedAt IS NULL, they are different rows, and the non-keeper is NOT noRevisado (that path already exists). APPROVAL GATE = canViewPractica, comisión-level. MARS-213 established that Mars has no pick-level gate anywhere and commons forbids introducing one to make a list match ownership. Any teacher of the comisión may approve. APPROVAL EXECUTION reuses deleteOwnPractica's cascade verbatim (practicaToothConductos -> practicaTeeth -> practicaImages -> practicas) in ONE transaction, with setArchiveContext(tx, approverUserId, "review:duplicate-delete-approved"). The review:* prefix is already accepted by .gitpush-pre.sh's gate, so this needs a new ROW in mars-archive-reasons.md and no gate change. The reason must name the approver, not the student - the student requested, the teacher acted. THE APPROVAL UI MUST SHOW BOTH COPIES' STATE AND GRADE before the teacher confirms. In 10 of the 21 strict groups more than one copy is already aprobado, so approving can retract an approved práctica and change the student's approved count. That is a design input, not a fork: the teacher-approval step IS the gate, and showing both rows is what makes it a real one. CÓDIGO GAP: stated, not re-opened. formatPracticaCodigo(date, seq) + MAX(seq)+1 reclaiming only the highest slot means a mid-sequence delete leaves that number permanently empty. The existing noRevisado self-delete already does this, so the decision was made implicitly when that shipped. DEMAND EVIDENCE, counts only, no ticket content: of 55 live support tickets, 4 mention "duplic" and 9 mention "elimin"/"borrar". Small and real, consistent with the 13-46 group population depending on the duplicate key. THREE QUESTIONS FOR THE PM. I will proceed on the stated default if no answer arrives, rather than block: (1) EMAIL vs in-app on each leg - default: in-app createNotification to the comisión's teachers on request, and ONE email to the student on decision through the existing mailer; no email on request, since a request is not urgent and the teacher rail already exists. (2) May a student request deletion of an APROBADO práctica, or only rectificar/desaprobado? Default: yes, aprobado included - 13 of 21 all-graded groups are the feature's whole point and restricting it would leave most duplicates unserviceable. (3) Is a rejection's reviewComment shown to the student? Default: yes, otherwise a rejected request is indistinguishable from a lost one.
-
PRE-IMPLEMENTATION DESIGN PING ANSWERED 2026-09-11, coder-mars-cc in the PM seat. GO on the design as specced. All three defaults CONFIRMED - proceed on them; they were the right calls and needed no change. (1) NOTIFICATIONS as defaulted: in-app to the comisión's teachers on request, ONE email to the student on decision, no email on request. Two constraints that ride on the email leg because it is the Class-A surface: resolve the recipient address LIVE at send time rather than from anything captured on the request row - a student whose address changed between request and decision must get the mail at the current one - and every link in that mail goes through resolveSiteOrigin(), never a bare env fallback. Both are existing Mars rules, restated here only because this is a NEW send site and a new site is where they get forgotten. (2) APROBADO PRÁCTICAS ARE ELIGIBLE. Confirmed. 13 of 21 all-graded groups are the feature's entire point and excluding them would ship a flow that cannot service the majority of its own population. The teacher-approval step is what makes this safe, and your requirement that the approval UI show BOTH copies' state and grade is the thing that makes the step a real gate rather than a rubber stamp - that is not optional, it is the control. (3) A REJECTION'S reviewComment IS SHOWN TO THE STUDENT. Confirmed, and your reason is the right one: otherwise a rejected request is indistinguishable from a lost one, which manufactures a support ticket for every rejection. Spanish, user-facing. TWO THINGS TO ADD, both correctness gaps at the schema/gate layer rather than product calls, which is what this ping is for: A. VALIDATE AT APPROVAL TIME, NOT ONLY AT SUBMIT. The spec lists submit-time checks (both prácticas are the requester's, both deletedAt IS NULL, different rows, non-keeper not noRevisado) and nothing at approval. The window between the two is real: the KEEPER can be soft-deleted through the existing deleteOwnPractica path while the request sits pending, and the teacher would then approve deleting the survivor of a pair whose keeper is already gone - a student left with neither copy, by a flow whose whole premise is that one is kept. Re-run the predicates inside the approval transaction and fail the approval with a named reason if any no longer holds. B. THE PARTIAL UNIQUE INDEX DOES NOT COVER THE MUTUAL CASE. It is on (practicaId) WHERE status is open, so A-keeps-B and B-keeps-A are two DIFFERENT practicaId values and both pass. Approve both and the student loses the pair. Reject at submit any request whose keeperPracticaId is itself the practicaId of an open request. Whether that is expressible as a constraint or has to live in the action is db-mars-cc's call; state which one it ended up being, because a check that lives in the action is a check the next writer can bypass. NOT RE-OPENED, per your framing and correctly: the código gap. The existing noRevisado self-delete already gaps mid-sequence numbers, so that decision landed implicitly when it shipped. State it in the student-facing copy so it is not a surprise; do not redesign the sequence for it. MECHANICS: db-mars-cc authors the DDL and the lookupOptions seeds for both new categories - DM it directly, I am not a relay. Its migration is deep, you review it. Your server action, the approval UI and the new mailer call are Class-A three ways, so the push takes build AND runtime log reads, and I review the diff. The new mars-archive-reasons.md row for review:duplicate-delete-approved lands in the SAME push as the action that uses it, never after. Design ping is GIVEN by this event. Do not wait for a second one.
-
Design approved; three defaults confirmed, two schema/gate additions attached.
-
SCHEMA AMENDMENT to my own spec, decided 2026-09-11 by coder02-mars-cc after db-mars-cc's DDL review. The PM's GO was given against the spec event above; these two columns changed AFTER it, so the amendment is recorded here rather than left implicit. TWO LOOKUP-BACKED COLUMNS BECOME TEXT VALUE CODES, NOT uuid FKs. statusOptionId -> status text NOT NULL, and reasonOptionId -> reason text NOT NULL, both storing the stable value code. Both vocabularies are still seeded in lookupOptions (practicaDeletionRequestStatus: pending/approved/rejected/cancelled; practicaDeletionReason: duplicate) for labels and for the server-side validator via getValidLookupValues. No CHECK on either value set. TWO INDEPENDENT REASONS, and the commons one is the stronger. (1) evolutiva-rulings-detail.md#lookups, Lookup-Backed Columns: a column whose domain is a lookupOptions group stores the option's stable value code, never the row uuid, and no DB-level FK is required - denormalized text is fine. So the uuid FK in my spec was the deviation from the fleet rule, and Mars's existing ...OptionId columns are inherited legacy rather than the pattern to copy. (2) db-mars-cc's measured finding: an index predicate cannot contain a subquery, so 'WHERE the status is open' is inexpressible against a uuid FK, and a hardcoded option uuid survives a vocabulary re-seed as a silent lie. My cited precedent did not transfer - they read uq_accessRequests_open_normalized live and accessRequests.status is PLAIN TEXT with no FK, predicate status = ANY(ARRAY['pending','needs_info']); the accessRequestStatus lookup category exists only for rendering. They then scanned all 12 partial indexes in public: not one predicates on an ...OptionId, every one is a text status or a NULL-ness/boolean column. reason follows status because a table where one lookup-backed column stores a code and the next stores a uuid is the shape the next author copies wrongly - 'it appears in no index predicate' is why it was not forced, not a reason to keep it. THE PAIR TRAP NOW CLOSES AT SCHEMA LEVEL AND MY CODE PATH IS DROPPED. db-mars-cc's solution, verified live in a temp table (the reversed insert was rejected, so LEAST/GREATEST are accepted in a uuid index expression): partial unique on (LEAST("practicaId","keeperPracticaId"), GREATEST("practicaId","keeperPracticaId")) WHERE status='pending' AND "deletedAt" IS NULL. A-keeps-B and B-keeps-A normalise to one key, and it blocks nothing legitimate - one práctica as keeper for several duplicates is pairs {A,C} and {B,C}, distinct. Better than the submit-time cross-check the PM asked for, so that check is replaced rather than kept alongside. Index 1 (one open request per non-keeper practicaId) STAYS: the pair index does not imply it. REJECTED, with the author's own objection as the reason: the resolvedAt-timestamptz fallback that would have preserved the uuid FK by predicating on WHERE "resolvedAt" IS NULL. It buys the constraint by adding a second thing every terminal transition must remember, and forgetting it over-blocks silently. Unchanged: everything else in the spec, and the PM's other two constraints - re-run the submit predicates INSIDE the approval transaction (the keeper can be self-deleted while the request sits pending), and resolve the email recipient LIVE at send time with links through resolveSiteOrigin().
-
db-mars-cc: DDL DONE. Migration 154 applied to prod and verified live; handoff commit 9e48efb (not pushed - db lane never pushes; coder02-mars-cc publishes). New table public.practicaDeletionRequests, 13 columns, 6 indexes, 2 structural CHECKs, 5 vocabulary rows. PM ITEM B ANSWERED, AND THE ANSWER IS CONSTRAINT, NOT ACTION - stated here because the decision asked for it explicitly. uq_practicaDeletionRequests_open_pair is UNIQUE (LEAST(practicaId, keeperPracticaId), GREATEST(practicaId, keeperPracticaId)) WHERE status='pending' AND deletedAt IS NULL, so A-keeps-B and B-keeps-A normalise to one key and the mutual request is refused by the database rather than by a code path the next writer can bypass. It blocks nothing legitimate: one keeper C for duplicates A and B is the pairs {A,C} and {B,C}, distinct. coder02-mars-cc's per-practicaId index is kept alongside it - one open request per non-keeper is a different rule that the pair index does not imply. TWO COLUMNS DIVERGE FROM THE SPEC TEXT, amended by coder02-mars-cc before the apply and recorded by it on this row: status and reason are text storing the stable lookupOptions value code, not uuid FKs. The forcing argument is item B itself plus the spec's own 'enforced in the schema not the action' - AN INDEX PREDICATE CANNOT CONTAIN A SUBQUERY, so 'WHERE the status is open' is not expressible against a uuid FK, leaving only a hardcoded uuid (which survives a vocabulary re-seed as a silent lie) or the action-level check the PM warned against. The precedent the spec cited turns out to agree: accessRequests.status is PLAIN TEXT with no FK and uq_accessRequests_open_normalized predicates on it, while the accessRequestStatus lookup category exists anyway for labels. Scan of every partial index in public: 12, and not one predicates on an ...OptionId. evolutiva-rulings-detail.md#lookups settles it independently of indexes and extends it to reason. NO FK TO lookupOptions(category, value): unavailability, not preference. uq_lookupOptions_category_value is a PARTIAL unique index and Postgres will not reference one from a foreign key. Caller validates via getValidLookupValues. No CHECK on either value set (native enum in disguise). THE POST-CHECK EXERCISES THE PAIR INDEX RATHER THAN DESCRIBING IT: it inserts a real request, then the mirrored one, and requires unique_violation, inside a plpgsql BEGIN/EXCEPTION subtransaction so the probe row rolls back with it; residue then asserted at 0 rows. Ran that way against prod. Práctica and user ids are read at run time, so no prod identifier is written into the file. GRANTS: mars_app_runtime = SELECT/INSERT/UPDATE, never DELETE (soft-delete only, matching accessRequests) - the caller must set deletedAt, not DELETE, or it gets a permission error rather than a bug. Correction to a fact both Mars lanes were carrying: the postgres-owned default ACL on public now gives a new table mars_app_runtime=r ONLY, not arwd, so grant lists in new-table migrations are load-bearing again rather than decorative. anon and authenticated get nothing. DELIBERATELY ABSENT, each stated in the file. No archive twin or trigger - not on the audited list, and the row is its own history; the audited tables are the ones the APPROVAL touches, and setArchiveContext(tx, approverUserId, 'review:duplicate-delete-approved') lands with the caller plus its mars-archive-reasons.md row in the same push. No CHECK tying a non-pending status to a reviewer - a student-side cancelled has no reviewer, so it would either forbid cancellation or fabricate one. No resolvedAt column. And PM ITEM A IS NOT A SCHEMA OBJECT AND CANNOT BE: the keeper can be soft-deleted through deleteOwnPractica while a request sits pending, and no constraint on this table can see a condition that lives on other rows at a later time - it must be re-run inside the approval transaction, and the header says so explicitly so the two indexes are not mistaken for covering it. Remaining scope on this row is entirely coder02-mars-cc's: submit and approval actions, the approval UI showing both copies' state and grade, the mailer call, the mars-archive-reasons.md row, and the push.
-
db-mars-cc: two reviewer-facing defects in migration 154's own text, found by self-review after the commit. NOT amended - 9e48efb is handed off to the push lane and an applied migration is immutable once another lane can read it; both are text, neither changes what is in prod. (1) The comment on ix_practicaDeletionRequests_keeper claims it 'keeps the second FK from being a sequential scan on a practicas delete'. That sentence is WRONG: the index is partial (WHERE deletedAt IS NULL) and Postgres's referencing-row check on a parent DELETE must find soft-deleted request rows too, so a partial index cannot serve that FK. The index is still useful for the approval UI's keeper-side lookup, which is the only reason it should be described as existing. Harmless in effect because trg_practicas_block_harddelete makes a practicas hard-delete unreachable, but a future reader would take the sentence as an FK-performance guarantee it does not provide. (2) The post-check asserted the grant with v_acl LIKE '%mars_app_runtime=%d%'. It passed only because mars_app_runtime happens to be the LAST aclitem in the string - any entry after it containing a 'd' would have made the check fail on a correct grant. Correct form is v_acl ~ 'mars_app_runtime=[^/]*d'. Same class as the migration-152 catch where a prosrc regex matched the explanatory comment rather than the code: a pattern that is right about the case in front of it and wrong about the property it claims to test. Neither is being carried into migration 155. Prod state is unaffected and unchanged: 13 columns, 6 indexes, the pair index proven by execution, 0 residue, 5 vocabulary rows, mars_app_runtime=arw (no DELETE - the caller sets deletedAt, it never issues DELETE).
-
PASS:9e48efb deep (coder02-mars-cc) - migration 154 reviewed and verified LIVE, not from the author's report: relacl is postgres=arwdDxtm, service_role=arwdDxtm, evolutiva_agent_ro=r, evolutiva_agent_rw=arwd, mars_app_runtime=arw (no DELETE); 6 indexes; 5 active vocabulary rows; 13 columns; 0 rows. db-mars-cc's two self-disclosed text defects confirmed and not re-spent (the ix_..._keeper FK comment, and the v_acl LIKE mars_app_runtime=%d post-check pattern). On the second, the live relacl shows the stake: evolutiva_agent_rw=arwd sits immediately BEFORE mars_app_runtime=arw, so the pattern was one aclitem-order away from ABORTING a correct migration. Fail-loud direction, no prod exposure. Correct form is v_acl ~ 'mars_app_runtime=[^/]*d'. ONE REVIEW FINDING, P3, on the header not the DDL. "THE PAIR TRAP, CLOSED IN THE SCHEMA" holds only for CONCURRENT pending mirrors. Both unique indexes are partial on status='pending', so once {A non-keeper, B keeper} is APPROVED its pair key leaves the index and {B non-keeper, A keeper} becomes insertable - the schema stops refusing the mirror. The sequential path is closed by a CALLER predicate instead: the approval soft-deletes A, so the mirrored request names a dead keeper and submit must reject it. The caller's obligation does not grow (same keeper-liveness check as PM item A), but the header reads as if the index covers the whole class. Bound to be recorded in 155's header or a COMMENT amendment; 154 is applied and immutable. CALLER CONSTRAINTS now carried by coder02-mars-cc for the MARS-147 flow: (1) soft-delete by setting deletedAt, never DELETE - mars_app_runtime holds no DELETE and the default ACL no longer supplies it since migration 144, so a DELETE is a permission error not a logic bug; (2) getValidLookupValues is the WHOLE validator for status and reason - no FK is possible because uq_lookupOptions_category_value is a partial unique index and Postgres cannot reference one; (3) re-run the submit predicates INSIDE the approval transaction (the keeper can be soft-deleted via deleteOwnPractica while a request sits pending - no constraint can see a condition on other rows at a later time); (4) re-read the request row FOR UPDATE in the approval transaction and re-check status='pending' there, or two approvers approve two different rows for one pair (db-mars-cc's sharpening); (5) setArchiveContext(tx, approverUserId, 'review:duplicate-delete-approved') plus the mars-archive-reasons.md row land with the caller in one push; (6) no CHECK ties a non-pending status to a reviewer (a student-side cancelled has none), so the caller enforces reviewer presence on approve/reject. Migration 154 is handoff commit 9e48efb, fourth in the ahead range awaiting coder02-mars-cc's push (9d8fb79, 16b9fed, 2ec6fa8, 9e48efb). Per coder-mars-cc's ordering the MARS-147 caller is LAST of four: MARS-553 -> MARS-213 -> MARS-720 -> MARS-147.
-
db-mars-cc: migration 155 APPLIED to prod 2026-09-11, commit HELD (coder02-mars-cc asked this lane to freeze handoff commits until its push of the four queued SHAs lands; an untracked file does not move the ahead range). Comments only - two COMMENT ON INDEX, no DDL, no data, no plan change, no schema.md re-export (the mirror carries no index comments and no object was added or dropped). Closes coder02's P3 on 9e48efb, which was right: migration 154's header sentence 'THE PAIR TRAP, CLOSED IN THE SCHEMA' is true of CONCURRENT mirrors and FALSE of sequential ones, and does not say which. ONE CASE WIDER THAN THE FINDING STATED: both unique indexes are partial on status='pending', so the key leaves the index on REJECT and CANCEL as well as on APPROVE. That release is CORRECT and is not being changed - if a rejected request kept the pair key, one bad submission would freeze a real duplicate pair permanently with no operator path back, which is a worse defect than the mirror it prevents. The partiality is load-bearing, not an oversight. HONEST STATEMENT OF THE GUARANTEE, now on the index itself rather than in a file header: concurrent mirror -> refused by the database, proven by execution in 154's post-check; sequential after APPROVE -> refused by the CALLER's keeper-liveness check, not by the database, because approval soft-deletes the non-keeper so the mirrored request names a soft-deleted keeper; sequential after REJECT or CANCEL -> deliberately allowed by both, since both practicas are still live and the mirror is a legitimate new request. Only the middle line is a caller obligation and it is the PM-item-A predicate the caller already carries, so the caller's list does not grow. SECOND COMMENT, not in the finding: uq_practicaDeletionRequests_open_practica now records why it is NOT implied by the pair index - the pair key is LEAST/GREATEST of the two ids, so {A non-keeper, B keeper} and {A non-keeper, C keeper} are two DIFFERENT keys and both can be pending simultaneously; only the per-practica index refuses the second. That is the reasoning behind keeping both indexes, moved from a DM into the catalog. Placed on the indexes rather than in a header on purpose: a header is read once by whoever opens that file, and the reader who gets this wrong is the one inspecting the table's indexes without ever opening migration 154.
-
PASS:bdedbc5 deep (coder02-mars-cc) - migration 155, two COMMENT ON INDEX, no DDL, no data, no plan change. Reviewed before publishing it, so the ahead range carries no unreviewed commit. The comments state the bound correctly and go one case further than my finding did: concurrent mirror refused by the database (proven by execution in 154's post-check); sequential-after-APPROVE closed by the CALLER's keeper-liveness predicate, not by the index; sequential-after-REJECT/CANCEL deliberately ALLOWED by both, because a key that never leaves the index would freeze a real duplicate pair permanently with no operator path back. The second comment records why uq_..._open_practica is NOT implied by the pair index ({A,B} and {A,C} are two different LEAST/GREATEST keys). Both sentences are on the INDEX rather than in a file header, which is right: the person who gets this wrong is reading the table's indexes and never opens migration 154. No findings. Published under coder02-mars-cc's MARS-720 push.
-
PARKED under the fleet-wide STOP (relayed Elazar, pmmaster-evolutiva-cc, 2026-09-11 23:57 ART). coder02-mars-cc stopping at a clean break; this is the single parking event, nothing started on this row. NOT DONE / NOT STARTED: MARS-147 itself. No code read, no branch, no commit, nothing in the tree. I was told to clear before starting it and the STOP arrived at that boundary, so the row is exactly as coder-mars-cc left it - status todo, assigned to me, two decision events plus three notes, thick enough to start cold by their own check. NEXT ACTION when Elazar's word resumes work: read MARS-147's decision events (they are the spec, not the title) and start from the two correctness gaps coder-mars-cc added rather than re-deriving them - (A) re-run the submit predicates INSIDE the approval transaction, because the keeper can be self-deleted via deleteOwnPractica while a request sits pending, leaving the student with neither copy; (B) the partial unique index is per-practicaId, so A-keeps-B and B-keeps-A both pass and approving both loses the pair. Class-A, DB schema: migration 154 is already applied, db-mars-cc leads any further DDL, I write the typed callers. Carried constraints on the row: soft-delete via deletedAt never DELETE; getValidLookupValues is the whole validator (no FK is available - a partial unique index cannot back one); re-read the request row FOR UPDATE and re-check status='pending' inside the transaction; setArchiveContext(tx, approverUserId, 'review:duplicate-delete-approved') plus the mars-archive-reasons.md row in ONE push; the caller enforces reviewer presence on approve/reject. coder-mars-cc wants cycle timestamps at both granularities (whole item, and the sub-cycle) on the close. DONE THIS WINDOW, all closed with verdicts on their own rows, nothing waiting on my context: MARS-720 closed (f976f74 v2.79.66, PASS deep, the rail formatter extracted pure and unit-tested). MARS-715 published for db-mars-cc (af916ae v2.79.67 carrying d7c666a, PASS:d7c666a deep, two P3s both accepted). MARS-722 filed, built and closed (63e4586 v2.79.68) plus its follow-up (6c90de9 v2.79.69, PASS shallow) - the LADDER_EPOCH drift test, forced red four ways total. MARS-64 published for db-mars-cc (97160d6 v2.79.70 carrying b55ba16, PASS:b55ba16 deep) with the supautils mechanism correction recorded on that row. MARS-723 filed as a decision row for coder-mars-cc. Five pushes, every one live-version confirmed against the REMOTE package.json, ending at v2.79.70 / dpl_Gx1aqKgqeGwCSJyUHMec4G8Q5iPt. STATE IS CLEAN, not broken: venus-host.push-gate RELEASED, ahead range EMPTY (no unpublished handoff of anyone's left in the shared tree, so neither push lane is blocked), no unread, no uncommitted work of mine anywhere in src/ db/ scripts/ or shared/. The dirty pm/ and agents/ paths in git status predate this session and belong to other lanes - deliberately not swept into a push. MARS-118 untouched and still blocked. MARS-724 is coder-mars-cc's row, not mine. Sending no ack per the STOP's own instruction.
task
2026-06-19 by wi-cli-venus
5w ago