remove-dead-misc-schemarun.md02_define/output/spec.mdThe demo-tenant data audit (.icm/intake/demo-data-quality/_source/audit-report.md, findings #6,
#7, #8, #9, #13, #15) proved a cluster of schema weight across five models that no code path
writes and no surface can show. It is not inert: activity.oldStatus/newStatus are ObjectId
refs to the retired status collection, populated on every feed read — a live
MissingSchemaError risk — and rendered by two UI components that can therefore never display
anything. Alongside them sit a wholly dead counter model, an invoice pdf field that stamps
every document with a princexml sample URL, an isSubmitted virtual testing a status the engine
does not have, a NOTIFICATION_TYPES enum whose UPPER_SNAKE values the real notification system
never writes, and status-history enum arms that are unwritable by construction.
Dead weight of this kind is what makes the demo tenant hard to trust and hard to seed: every
reader has to work out which fields are real. This run is step 5 of the demo-data-quality intake
batch, which serves Refine the bridge / Q2-2026 Objective 1 — Establish Product-Market Fit with
Vendor Partners by making the demo world an honest, leanly-modelled representation of the
product. Removing the weight now also shrinks the surface the later seeding runs (stubs 9 and 10)
have to reason about.
One cleanup run, no behaviour change any persona can observe. Grouped by model; each group is its own commit.
counter — delete the model. getNextId has zero callers (lead.requestId is allocated from
tenant.leadRequestSequence); the only consumer is the demo reset purging an always-empty
collection. Delete models/counter.ts and its models/index.ts re-export, drop "counter" from
demo-reset/policy.ts PURGED_MODEL_FILES (and the paragraph documenting why it was listed),
drop its PURGE_TARGETS entry and import in demo-reset/index.ts, and drop its admin-facing
label from demo-reset-button.tsx. The drift test walks model files, so deleting the file
removes the classification obligation. Reword the leadRequestSequence reset comment, which
currently explains itself by reference to the counter collection. The migration drops the
counters collection.
invoice — drop pdf and two dead virtuals. pdf has zero readers and defaults every
document to https://www.princexml.com/samples/invoice/invoicesample.pdf. isSubmitted tests
status === "invoice_submitted", which is not one of the engine's invoice statuses
(invoice_draft/sent/disputed/paid/rejected); isPaid has zero readers. Remove the field, its
IInvoice member, and both virtuals; $unset pdf in the migration.
activity — cut the retired-status refs and the dead audit columns. Remove oldStatus and
newStatus from IActivity, IActivityPopulated and the schema; remove POPULATE_STATUSES and
its four call sites in services/activity/index.ts; remove the status-change render blocks from
activity-feed.tsx and activity-detail.tsx. In toCustomerActivityEntry the statusLabel
derivation goes with them — the event label falls through to the humanised action name and then
"Project update", and DELIVERY_STATUS_HINT tests the action name alone. Because newStatus is
never written, statusLabel is already always undefined, so this is a code simplification with
no output change. Also remove fieldChanged/beforeValue/afterValue (zero refs) and userId
(never written, yet doubly indexed — an inline index: true plus a compound), and drop the inline
index: true on leadId, which is a redundant prefix of the {leadId, createdAt: -1} compound
the queries actually use. Status changes keep rendering from message/metadata, as they already
do.
notification — delete the dead enum. NOTIFICATION_TYPES and NotificationType have zero
consumers outside the model; the live system writes dot.case strings from
notifications/*/types.ts. Delete both and type INotification["type"] as AnyNotificationType
from notifications/preferences.js — a type-only import, erased at runtime, and the */types.ts
modules it transitively names import nothing, so no cycle is introduced. No migration: the stored
values are already the dot.case ones.
skill — drop metadata. Zero readers, zero writers. $unset in the migration.
status-history — narrow to what is actually written. entityType becomes the literal
"lead" in both the TS type and the schema enum; bid and finance were never written and are
unpopulatable, and "lead" is the only value any write path emits. entityId loses
refPath: "entityType" and becomes a plain Schema.Types.ObjectId — nothing anywhere populates
it, and the refPath is machinery inherited from the retired status-collection era. Narrow
buildLeadProgressItems's workflow parameter from Workflow to "lead" (its one caller,
leads/index.ts:968, already passes "lead") and drop the now-unnecessary
as IStatusHistory["entityType"] cast. Remove the dead metadata field and the unused bare
{status: 1} index. comment stays — it is write-only today but is a deliberate audit column,
not proven dead.
One migration carries the database side: drop the counters collection, $unset the six
removed data-bearing fields on their collections, and drop the four indexes
(activities.userId_1, activities.userId_1_createdAt_-1, activities.leadId_1,
statushistories.status_1). down is symmetric — recreate the indexes; the $unsets are not
re-populated (the data was default-or-absent), which the migration documents inline.
packages/services/src/db/models/counter.ts is deleted and no longer re-exported from
models/index.ts; getCounterModel has zero references repo-wide.demo-reset/policy.ts no longer lists "counter", demo-reset/index.ts has no counter
purge target, and demo-reset-button.tsx has no counter label; policy.test.ts passes in
CI unchanged (the drift guard is satisfied by the file's absence, not by an exclusion).invoice.pdf, isSubmitted and isPaid are gone from the model; no invoice surface
changes (no reader existed).ActivityService read populates oldStatus/newStatus; grep -r "oldStatus\|newStatus"
over packages/services/src and apps/web returns only the unrelated local variable in
services/leads/index.ts.activity.fieldChanged, beforeValue, afterValue and userId are gone from IActivity
and the schema; leadId keeps only the {leadId, createdAt: -1} compound.NOTIFICATION_TYPES and NotificationType are deleted; INotification["type"] is
AnyNotificationType, and packages/services typechecks with no cyclic-import error.skill.metadata is gone from ISkill and the schema.status-history.entityType is the literal "lead" in type and schema enum; entityId has
no refPath; metadata and the {status: 1} index are gone; buildLeadProgressItems
takes "lead" and carries no entityType cast.up/down: drops the counters collection,
$unsets invoice.pdf, skill.metadata, status-history.metadata and activity's
oldStatus/newStatus/fieldChanged/beforeValue/afterValue/userId, and drops the
four named indexes. Index drops use the existing dropIfExists idiom so a re-run is safe.statushistories documents carrying a legacy entityType are left in place — the
narrowed enum validates writes only, and no write path rewrites them. Stated in the
migration's header comment so the next reader does not mistake it for an oversight.paid/paidAt/isApproved dual-write of the status machine, and dueDate's missing
write path — live behaviour, needs its own scope (audit finding #8).amount ≡ total, isDraft ≡ status) and
quote.pdf — live write paths, same reason (finding #16).status-history.comment — write-only today, but a deliberate audit column; not proven dead.workflows, status and nine other zombie collections, and the deleted-tenant
production residue — destructive prod migrations deferred by decision D2 to the separate
db-audit-findings triage. Note there is no workflow Mongoose model in the repo; the live
status engine is the static packages/services/src/db/workflows/ module (workflows.json +
index.ts), which this run does not touch.$indexStats window is too weak to act on (D1).
Only the four indexes named above are dropped, each because its field is being removed or it is
a strict prefix of a compound that remains.03_build/output/notes.md87c18f7 refactor — delete the dead counter model06a5ccf refactor — drop invoice pdf and the dead virtualsaca3474 refactor — cut activity's retired-status refs and dead columns22ac842 refactor — notification enum, skill and status-history88d0d39 feat — migration for the removed fields and indexesdb/models/counter.ts: deleted. getNextId had zero callers; lead.requestId comes from
tenant.leadRequestSequence. Its models/index.ts re-export, demo-reset/policy.ts entry (and
the paragraph explaining why it was listed), demo-reset/index.ts purge target and import, and
the admin PURGED_LABELS entry in demo-reset-button.tsx all went with it. The
leadRequestSequence reset comment no longer explains itself by reference to a collection that
does not exist.db/models/invoice.ts: pdf field, its IInvoice member, and the isSubmitted/isPaid
virtuals removed.db/models/activity.ts: oldStatus, newStatus, fieldChanged, beforeValue, afterValue
and userId removed from IActivity, IActivityPopulated and the schema. The bare
{ leadId: 1 } inline index dropped as a redundant prefix of { leadId, createdAt: -1 };
a comment records why and points at the migration.db/services/activity/index.ts: POPULATE_STATUSES and its four call sites removed.
toCustomerActivityEntry no longer derives a statusLabel — the event label falls through to
the humanised action name, and DELIVERY_STATUS_HINT tests the action name alone.apps/web/components/activity/activity-feed/activity-feed.tsx,
.../activity-detail/activity-detail.tsx, apps/web/lib/queries/workspace.ts: the three
status-change render paths removed.db/models/notification.ts: NOTIFICATION_TYPES and NotificationType deleted;
INotification["type"] is now AnyNotificationType (type-only import from
notifications/preferences.js).db/models/skill.ts: metadata removed.db/models/status-history.ts: entityType narrowed to the literal "lead" in both the TS type
and the schema enum; refPath dropped from entityId; metadata and the bare { status: 1 }
index removed, with a comment pointing at the migration.db/services/status/build-progress-items.ts: buildLeadProgressItems takes workflow: "lead"
and passes it straight through — the as IStatusHistory["entityType"] cast and the now-unused
Workflow / IStatusHistory imports are gone.db/migrations/1786665600000-remove-dead-misc-schema.ts: new.The spec's Proposed change listed two activity render paths (activity-feed.tsx,
activity-detail.tsx); there is a third — apps/web/lib/queries/workspace.ts:340-345, which
resolved a persona-aware label from entry.newStatus?.name and fell back through
newStatus?.displayName. It is the same dead field, so statusEvent was always null and the
chain already landed on humanizeAction(entry.action?.name) || entry.message || "Update" —
removing it changes no output. Handled here rather than deferred, because the spec's acceptance
criterion is explicit that a repo-wide grep must come back clean; the criterion covers it even
though the prose enumerated only two files.
counter.ts deleted, not re-exported, getCounterModel has zero references repo-wide —
verified by grep.policy.ts, demo-reset/index.ts and demo-reset-button.tsx carry no counter;
policy.test.ts unchanged. Its third case ("classifies nothing that is not a tenant-scoped
model") is what would have caught a stale name left behind, so the existing test proves the
removal is complete rather than needing an edit.Record keyed on PURGED_MODEL_FILES, so a missing label is a compile
error, but the run itself is Verify's smoke.invoice.pdf, isSubmitted, isPaid gone; no invoice surface changed (no reader existed).ActivityService read populates the statuses; no surface renders a status-change block.
grep -rn "oldStatus\|newStatus" over packages/services/src and apps/web now returns
only the newStatusName local in services/leads/index.ts, as the criterion specifies.fieldChanged, beforeValue, afterValue, userId gone; leadId keeps only the compound.NOTIFICATION_TYPES/NotificationType deleted; INotification["type"] is
AnyNotificationType. No cycle: preferences.ts imports only the per-domain types.ts
modules, which import nothing, and the import is type-only so it is erased at runtime.skill.metadata gone.status-history.entityType is "lead" in type and enum; no refPath; metadata and
{status: 1} gone; buildLeadProgressItems takes "lead" and carries no cast.counters, $unsets the eight fields,
drops the four indexes via a dropIndexIfExists helper; down recreates all four indexes.entityType documents left in place, stated in the migration's header.Quality check run and the Vercel preview.down is a deliberate partial inverse. It recreates the four indexes but does not
repopulate the unset fields or restore the counters documents, and it does not throw. The
reasoning is spelled out in the migration header: the removed values were the schema default or
absent, and throwing would make the index rollback unreachable. This is the same shape as
1784400000000-remove-service-fee.ts — loud header, usable down. Worth a look if you disagree
with the trade.unsetFields is not tenant-scoped, on purpose. These fields are retired for every tenant at
once. Flagged because AGENTS.md requires bulk updates to be tenant-safe when they should be
per-tenant — this one should not be, and says so inline.1786665600000 sits one day after the current head migration
(1786579200000-reflag-demo-tenant-preview). Nothing else in the demo-data-quality batch has
merged yet, so no ordering conflict today — but stubs 1, 2 and 4 also carry migrations and are
meant to run in parallel. If one of them lands first with a higher timestamp, this stays
correctly ordered regardless; ts-migrate-mongoose tracks by name, not by position.[ ] above rather than dropped: the
demo-reset smoke on the preview, and the CI check rollup.activity-detail grid keeps grid-cols-2 sm:grid-cols-3 with a single remaining cell.
Left as-is — restyling the panel is outside this run's scope.Context budget: within the Inputs table.
04_verify/output/verify.md88af6f2), 5 fed to intake. No blockers.complexity: standard) — 1 finding, not fixed (judgement call, below).
The CI Review diff against CONVENTIONS.md job reported skipped, so the review was run here.ROUTE_POLICIES is untouched; no new routes, server actions, cron routes or external callbacks;
the only auth-adjacent file is demo-reset-button.tsx, and only to delete one label string.Production-readiness found that the original migration's drop of statushistories.status_1 was
justified by the wrong test. The Build-stage reasoning was "no query in code seeks on status
alone" — but a single-field index serves any query carrying an equality predicate on that
field, whatever else it filters on. vendor/index.ts::loadDeliveredSpans matches
{tenantId, entityType, status, changedAt} with no entityId, which makes it the one
statushistories read the surviving {tenantId, entityId, entityType, changedAt} compound cannot
serve past its tenantId prefix. Dropping status_1 outright would have left that aggregate
filtering the whole tenant's status history in memory — on a single-dominant-tenant database,
effectively a full collection scan.
Fixed in 88af6f2: the migration now creates a tenant-leading
{tenantId, status, changedAt: -1} before dropping the bare index, and down reverses both in the
correct order. The replacement is ESR-correct for that match and a better fit than the index it
retires — the old one did not lead with tenantId, against house convention.
This is a deviation from the letter of the spec, which said the migration "drops the four named indexes". It now drops four and creates one. The deviation serves the spec's intent (remove dead weight without regressing live queries) rather than contradicting it, but it is Jamie's to accept.
Preview: https://web-git-claude-remove-dead-misc-schema-d7k9pp-sustentus.vercel.app — responds
HTTP 200 unauthenticated (agent).
counter.ts deleted, not re-exported, getCounterModel zero references — repo-wide grep
clean across apps/* and packages/*/src; confirmed independently by both reviews (agent)policy.ts / demo-reset/index.ts / demo-reset-button.tsx carry no counter;
policy.test.ts unchanged and green — Quality → Run tests passed (agent + CI)PURGE_TARGETS is a Record keyed on
PURGED_MODEL_FILES, so a missing entry is a compile error, and typecheck is green. That is
not the same as watching a reset run.invoice.pdf, isSubmitted, isPaid gone; no invoice surface changed (no reader existed
before or after) (agent)ActivityService read populates oldStatus/newStatus; the three render paths are gone.
grep -rn "oldStatus\|newStatus" over packages/services/src + apps/web returns only the
newStatusName local in services/leads/index.ts, exactly as the criterion specifies (agent)activity.fieldChanged/beforeValue/afterValue/userId gone; leadId keeps only the
{leadId, createdAt: -1} compound (agent)NOTIFICATION_TYPES/NotificationType deleted; INotification["type"] is
AnyNotificationType; no cyclic-import error — typecheck green (agent + CI)skill.metadata gone (agent)status-history.entityType is "lead" in type and enum; no refPath; metadata gone;
buildLeadProgressItems takes "lead" with no cast (agent)dropIndexIfExists — and it actually
ran: the Migrate preview database check passed on this branch, which is stronger evidence
than a code read (agent + CI). Note the create-before-drop amendment above.entityType documents left in place, stated in the migration header. Both reviews
independently confirmed no read path save()s one back through the narrowed enum — every
read is .lean() or an aggregate (agent)Quality Project success. Re-running against
88af6f2; the result must be green before Ship (agent + CI)pdf/virtuals are display-layer dead weight, not the
payment state machine, which this run explicitly leaves alone (agent)notifyX call and changes no send path. create.ts now types type as the union the
senders already pass, so every existing notification keeps firing identically (agent)Walkthrough clip: not posted. The contract asks for one on owner-facing changes; this run has no observable behaviour change for any persona by design, so there is nothing to show. Say the word if you want one of the activity surfaces recorded anyway.
Fixed on branch (88af6f2):
statushistories.status_1 drop regressed loadDeliveredSpans — replaced with a tenant-leading
index, as above. The one genuine bug this stage found.INotification["type"] narrowing was unenforced at the write boundary —
CreateNotificationInput["type"] was a bare string, so the model's "senders cannot drift
apart" comment was not actually true. Now typed AnyNotificationType. (send-email.ts already
cast to this union in four places, which is good evidence the union was always the intent.) * comment line left in policy.ts by the counter removal.Not fixed — judgement call for you:
apps/web/components/notifications/notification-inbox-list.tsx:38-53 — the typeAccent map is
keyed on PROPOSAL_RECEIVED, INVOICE_PAID, SLA_BREACH …, i.e. exactly the UPPER_SNAKE
constants this PR deletes. Senders have written dot.case for some time, so typeAccent[n.type]
has always fallen through to DEFAULT_ACCENT and every inbox row renders the same blue border.
Both reviews found this independently.
I did not fix it here. The map is dead in the same way the enum was, but repairing it changes visible UI — rows would start rendering distinct accent colours — and this run's spec promises no behaviour change any persona can observe. Picking those colours is a design decision, not a cleanup. It belongs in intake, which is also where production-readiness put it. Tell me if you'd rather it rode along here.
Fed to intake (out of this run's scope, recorded so they are not lost):
humanizeAction yields "Brd flagged", "Qualified out". Unchanged by this PR (the
newStatus branch was always null), but the comment removed at that site was the only in-repo
record that the label is meant to be persona-safe via statusService.resolveLabel.unsetFields on activities is a collection scan — six $exists predicates with no supporting
index, run on merge. Fine at current volume; worth knowing before the collection grows.apps/docs/.../lead-intake/page.mdx:51 still says requestId "is generated via an atomic
counter" — the collection this PR drops. Pre-existing drift (allocation moved to
tenant.leadRequestSequence earlier), but this run makes it plainly wrong. docs-sync at Ship
should reconcile it — flagging here so Ship does not miss it.paid/paidAt/isApproved dual-write remain out of
scope, as the spec states.Process note: the first /code-review pass ran against a stale local main ref (67e3353,
behind origin/main at 87183b0) and reviewed 345 files of already-merged history, returning three
findings that were not from this PR. Caught by the diff size, re-run against origin/main...HEAD
(21 files). Local main has been fast-forwarded so it does not recur.
Context budget: within the Inputs table.
05_ship/output/investor-update.mdWho it's for: Admin, and every persona indirectly What shipped: A dead database model removed, plus unused fields, indexes and enums across five more, with a migration that clears them from the live database. Why it matters: Refine the Bridge — fewer places a real bug can hide.
One removal mattered: activity records referenced a retired collection on every read.
Dig deeper: https://github.com/sustentus/sustentus/pull/812
05_ship/output/release.mdf171c45 — Quality Project (format · lint · typecheck · test),
Migrate preview database, Audit database, and the four advisory pipeline checks all passed.
Migrate production database and Review diff against CONVENTIONS.md skipped by design.apps/docs/app/technical/demo-environment/page.mdx — "via the tenant counter"
→ "via tenant.leadRequestSequence", matching what the same page already said correctly
further down.apps/docs/app/business/service-journey/lead-intake/page.mdx — the requestId
section said the number "is generated via an atomic counter", which named the collection this
run deletes. Now: allocated atomically from a sequence held on the tenant. The allocation is
still genuinely atomic (an aggregation-pipeline $inc in allocate-next-lead-request-id.ts),
so only the word "counter" was wrong.ship-note.yaml fires on this merge and emails the note to #product-update.
Not sent at the time of writing; the merge is the trigger.counter.ts deleted, not re-exported, getCounterModel zero references — repo-wide grep,
confirmed independently by the production-readiness and code-review passespolicy.ts / demo-reset/index.ts / demo-reset-button.tsx carry no counter;
policy.test.ts unchanged and green in CIinvoice.pdf, isSubmitted, isPaid gone; no invoice surface changedActivityService read populates oldStatus/newStatus; all three render paths removed
(feed, detail, and the workspace.ts path the spec's prose did not enumerate)activity.fieldChanged/beforeValue/afterValue/userId gone; leadId keeps only the
{leadId, createdAt: -1} compoundNOTIFICATION_TYPES/NotificationType deleted; INotification["type"] is
AnyNotificationType; no cyclic import — typecheck greenskill.metadata gonestatus-history.entityType is "lead"; no refPath; metadata and the bare {status: 1}
index gone; buildLeadProgressItems takes "lead" with no castMigrate preview database passed
against a real database, twice (before and after the Verify amendment)entityType documents left in place, stated in the migration headerThe spec said the migration "drops the four named indexes". It drops four and creates one.
Production-readiness found that dropping statushistories.status_1 would regress
vendor/index.ts::loadDeliveredSpans — the one statushistories read with no entityId, which a
bare {status: 1} was serving. The migration now creates a tenant-leading
{tenantId, status, changedAt: -1} before dropping the old index. Recorded in verify.md, and
accepted by the same tick that authorised the merge.
notification-inbox-list.tsx keys its accent map on the UPPER_SNAKE constants this run deletes,
so every inbox row renders the default accent. Not fixed here: repairing it changes visible UI in
a run that promises no observable change, and the colour choices are a design decision.Brd flagged, Qualified out).unsetFields on activities is a collection scan — fine at current volume.paid/paidAt/isApproved dual-write, both out of
scope per the spec.Context budget: within the Inputs table.