The same code, before and after.
Left: the Salesforce your org runs today. Right: the owned stack you keep after migration — same behavior, line-referenced back to the source, and flagged where a human must confirm. The Apex class below is the lead exhibit; LWC, Flow, workflow rules, and triggers follow.
ListExperiencesFromType — an @InvocableMethod from Salesforce's Coral Cloud sample org, translated by the engine. Same behavior, same query semantics, line-referenced back to the source.
STATUS: VERIFIED · 0 FLAGS · RE-TRANSPILED CLEAN + SAFETY-SCANNED → PROMOTEDAPEX (SALESFORCE) ListExperiencesFromType.cls · 79 lines
public with sharing class ListExperiencesFromType { @InvocableMethod( label='List Experiences from Type' description='Returns a list of experiences…' ) public static List<ExperienceListWrapper> getExperiencesFromType( List<ExperienceTypeInput> experienceTypeInputs ) { List<ExperienceListWrapper> experienceWrappers = new List<…>(); for (ExperienceTypeInput experienceTypeInput : experienceTypeInputs) { String type = '%' + experienceTypeInput.type.toLowerCase().trim() + '%'; // Fetch experiences records List<Experience__c> experiences = [ SELECT Id, Name, Picture_URL__c, Description__c FROM Experience__c WHERE Type__c LIKE :type WITH SYSTEM_MODE ]; ExperienceListWrapper experienceListWrapper = new …(); for (Experience__c experience : experiences) { experienceListWrapper.experiences.add( new ExperienceItem( experience.Name, experience.Picture_URL__c, 'image/jpeg', experience.Description__c)); } experienceWrappers.add(experienceListWrapper); } return experienceWrappers; } public class ExperienceTypeInput { @InvocableVariable(… required=true) public String type; } public class ExperienceListWrapper { public List<ExperienceItem> experiences; … } public class ExperienceItem { public String name; imageUrl; mimeType; description; … } }
TYPESCRIPT (YOURS) services/ListExperiencesFromType.ts · 70 lines
import { PrismaClient } from "@prisma/client"; import { z } from "zod"; // Apex inner class ExperienceTypeInput (line 40) — required=true export const experienceTypeInputSchema = z.object({ type: z.string(), }); // Apex inner class ExperienceItem (line 61) export interface ExperienceItem { name: string | null; imageUrl: string | null; mimeType: string | null; description: string | null; } // Apex inner class ExperienceListWrapper (line 49) export interface ExperienceListWrapper { experiences: ExperienceItem[]; } // Pre-translated SQL from the substitution table (Apex line 17) — verbatim. const SQL_EXPERIENCES_BY_TYPE = 'SELECT "Experience"."id", "Experience"."name", "Experience"."picture_URL", "Experience"."description" FROM "Experience" WHERE "Experience"."type" ILIKE $1'; // Apex: getExperiencesFromType (lines 6-38) — @InvocableMethod // exposed as a service function for the automation runtime. export async function getExperiencesFromType( experienceTypeInputs: ExperienceTypeInput[] ): Promise<ExperienceListWrapper[]> { const inputs = z.array(experienceTypeInputSchema).parse(experienceTypeInputs); const experienceWrappers: ExperienceListWrapper[] = []; // The Apex source runs one query per input (query inside the // loop, lines 10-36); that batching is preserved exactly. for (const experienceTypeInput of inputs) { const type = "%" + experienceTypeInput.type.toLowerCase().trim() + "%"; const experiences = await prisma.$queryRawUnsafe<ExperienceRow[]>( SQL_EXPERIENCES_BY_TYPE, type); const experienceListWrapper: ExperienceListWrapper = { experiences: [] }; for (const experience of experiences) { experienceListWrapper.experiences.push({ name: experience.name, imageUrl: experience.picture_URL, mimeType: "image/jpeg", description: experience.description, }); } experienceWrappers.push(experienceListWrapper); } return experienceWrappers; }
- SOQL → SQL, pre-translated and pinned. The query is converted by the deterministic SOQL compiler first (LIKE :type → parameterized ILIKE $1 — Salesforce LIKE is case-insensitive, Postgres LIKE is not, so the engine picks ILIKE for parity). The LLM must use that SQL verbatim — it cannot invent queries.
- Inner classes → typed contracts. @InvocableVariable required=true becomes a Zod schema that validates at the boundary; plain inner classes become interfaces. Every block carries a line reference back to the Apex source.
- Behavior preserved, not "improved." The Apex runs one query per input inside a loop — a reviewer might call that inefficient, but the translation keeps it and says so in a comment. Parity first; optimization is a human decision later.
- Verification, then promotion. The output is re-transpiled strictly and safety-scanned in-process. This class came back clean with 0 flags → promoted to services/ as the canonical implementation. Classes with flags stay in services/drafts/ with line-referenced reasons — and that flagged list is the plan our migration engineers run, a person in the lead, AI as leverage.
- WITH SYSTEM_MODE → the security model moves, too. Sharing/FLS semantics don't ride inside each query anymore; they are enforced by the generated RBAC + row-level-security layer at the API and database.
The limits are gone. On Salesforce, your own code runs inside a meter: 100 database queries per transaction, 150 writes, ten seconds of CPU, six megabytes of memory — hard ceilings you can't raise at any price. A large part of every Salesforce codebase exists only to dodge those ceilings: queries shuffled out of loops, jobs split into 200-record batches, work shoved into async just to buy a bigger budget. When we move you to your own stack, that entire tax disappears. There's no query counter, no CPU kill-switch, no per-transaction wall — a single request can read and write as much as the work actually requires. You still have real limits, because every system does: a database connection pool, a query timeout, the memory on your server. The difference is that you own and tune every one of them, sized to your workload and your budget — instead of a platform meter sized to protect someone else's multi-tenant cluster. You stop engineering around the platform and start engineering for your business.
| On Salesforce (the meter) | What your team built to dodge it | On your own stack |
|---|---|---|
| SOQL — 100 queries / tx | Queries hoisted out of loops; map-keyed lookups to stay under the counter. | No query counter — a request issues as many as the work needs. |
| CPU — 10 s kill | Work shoved into async (@future / Queueable) to escape the 10-second kill. | No CPU kill-switch — the handler runs to completion. |
| DML — 150 writes / tx | Never write inside a loop; collect into a list and do one bulk DML. | No write counter — one batched write, no per-transaction row cap. |
| Batch — 200-record chunks | Database.Batchable plumbing to page around the row / heap / CPU caps. | A design choice, not a requirement — often one set-based SQL. |
The honest companion: real limits remain — a database connection pool, a query timeout, your server's memory. Every one is yours to tune, sized to your workload; none is a platform meter that aborts your business logic mid-save.
paginator — a Coral Cloud list-pager component. Its markup converts automatically; the @api properties become typed props, the getters become derived constants, and each CustomEvent becomes an onX callback.
LWC (SALESFORCE) paginator · html + js
<template> <div class="slds-grid slds-grid_vertical-align-center"> <div class="slds-col slds-size_2-of-12"> <lightning-button-icon lwc:if={isNotFirstPage} icon-name="utility:chevronleft" onclick={handlePrevious}></lightning-button-icon> </div> <div class="slds-col …8-of-12 nav-info"> {totalItemCount} items • page {currentPageNumber} of {totalPages} </div> <div class="slds-col slds-size_2-of-12"> <lightning-button-icon lwc:if={isNotLastPage} icon-name="utility:chevronright" onclick={handleNext}></lightning-button-icon> </div> </div> </template> // paginator.js import { LightningElement, api } from 'lwc'; export default class Paginator extends LightningElement { @api pageNumber; @api pageSize; @api totalItemCount; handlePrevious() { this.dispatchEvent(new CustomEvent('previous')); } handleNext() { this.dispatchEvent(new CustomEvent('next')); } get currentPageNumber() { return this.totalItemCount === 0 ? 0 : this.pageNumber; } get isNotFirstPage() { return this.pageNumber !== 1; } get isNotLastPage() { return this.pageNumber !== this.totalPages; } get totalPages() { return Math.ceil(this.totalItemCount / this.pageSize); } }
REACT + TYPESCRIPT (YOURS) converted/paginator.tsx · 55 lines
// Converted from LWC "paginator" — deterministic LWC→React. // Every construct traced to the directive table. import type { ReactNode } from "react"; export interface PaginatorProps { pageNumber?: any; // @api pageSize?: any; // @api totalItemCount?: any; // @api onPrevious?: (detail?: any) => void; // custom event onNext?: (detail?: any) => void; // custom event } export function Paginator(props: PaginatorProps): ReactNode { const { pageNumber, pageSize, totalItemCount, onPrevious, onNext } = props; const currentPageNumber = totalItemCount === 0 ? 0 : pageNumber; const isNotFirstPage = pageNumber !== 1; const totalPages = Math.ceil(totalItemCount / pageSize); const isNotLastPage = pageNumber !== totalPages; const handlePrevious = () => { onPrevious?.(); }; const handleNext = () => { onNext?.(); }; return ( <> <div className="slds-grid …vertical-align-center"> <div className="slds-col slds-size_2-of-12"> {isNotFirstPage ? ( <button className="slds-button slds-button_icon" onClick={handlePrevious}> <span className="slds-assistive-text" /> </button> ) : null} </div> <div className="slds-col …8-of-12 nav-info"> {totalItemCount} items • page {currentPageNumber} of {totalPages} </div> <div className="slds-col slds-size_2-of-12"> {isNotLastPage ? ( <button className="slds-button slds-button_icon" onClick={handleNext}> <span className="slds-assistive-text" /> </button> ) : null} </div> </div> </> ); }
Source: coral-cloud/…/lwc/paginator/{paginator.html, paginator.js} → web/src/components/converted/paginator.tsx (55 lines, strict-tsc-verified). Template converted from the deterministic directive table, no AI. Corpus: 278 components measured → 23 converted (strict tsc, 0 diagnostics each) · 108 partial (template automatic, handlers flagged) · 147 flagged. Aura components are honestly manual.
A PMM flow that auto-names Program Engagement records. The engine emits the trigger contract, variable bindings, and decision skeleton mechanically. Salesforce formula expressions it can't yet convert are marked OPENFORCE-TODO and fail closed — typed undefined, never a guessed value.
FLOW (SALESFORCE) …Program_Engagement.flow-meta.xml · 800 lines
<Flow xmlns="http://soap.sforce.com/2006/04/metadata"> <decisions> <name>myDecision</name> <label>myDecision</label> <defaultConnector> <targetReference>myDecision3</targetReference> </defaultConnector> <rules> <name>myRule_1</name> <conditionLogic>and</conditionLogic> <conditions> <leftValueReference>formula_myRule_1</leftValueReference> <operator>EqualTo</operator> <rightValue> <booleanValue>true</booleanValue> </rightValue> </conditions> <connector> <targetReference>myRule_1_A1</targetReference> </connector> <label>Update Name</label> </rules> </decisions> <!-- …800 lines: 6 decisions, 5 formulas, assignments… --> </Flow>
TYPESCRIPT (YOURS) flows/…Program_Engagement.ts · 56 lines
/** * AUTO-GENERATED by openforce — Salesforce Flow * "Template: Program Engagement Object" → TypeScript. * * TRIGGER CONTRACT — consumed by the app's hook system: * exports `trigger` + `run`. afterSave/beforeSave: run(prisma, record) * fires for events in trigger.on; same-record writes accumulate on the * returned `patch` (persisted without recursive re-triggering). * * TRANSLATION NOTES: * - Formulas computed once at entry (SF evaluates lazily). * - // OPENFORCE-TODO(...) marks constructs with no deterministic (or * LLM-assisted) translation; see the `flagged` list. */ import type { PrismaClient } from "@prisma/client"; export const trigger = { timing: "manual" } as const; export async function run(prisma: PrismaClient): Promise<void> { let myVariable_current: ProgramEngagement | null = null; // Flow formulas (computed once at entry — see header) // OPENFORCE-TODO(formula): formula_myRule_1 — requires translation. // expression: NOT({!myVariable_current.AutoName_Override__c}) const formula_myRule_1: boolean = undefined as unknown as boolean; // OPENFORCE-TODO(formula): formula_5_myRule_4 — requires translation. // expression: IF(NOT(ISNULL({!…StartDate__c})), // IF({!…StartDate__c} > TODAY(), TODAY(), NULL), TODAY()) const formula_5_myRule_4: Date = undefined as unknown as Date; // (flow has no start connector — nothing to execute) }
Source: PMM/…/flows/Auto_Populate_Date_And_Name_On_Program_Engagement.flow-meta.xml (800 lines) → flow-example.ts (56 lines), generated keyless. Deterministic-first, with key-gated LLM assist for the gaps — and every gap is listed per flow (here: 5 formulas outside the mechanical subset), never silently dropped. A construct with no safe translation becomes a typed undefined that fails a type check until a human resolves it, rather than a plausible-looking wrong value.
NPSP's Opportunity workflow becomes a pure TypeScript rule engine. Field updates are applied as a patch the caller persists; every email alert, task, and outbound message surfaces as a pendingManualActions entry. The engine never silently sends anything.
WORKFLOW (SALESFORCE) Opportunity.workflow-meta.xml
<Workflow xmlns="…/2006/04/metadata">
<fieldUpdates>
<fullName>Opportunity_Copy_FMV_to_Amount</fullName>
<field>Amount</field>
<formula>Fair_Market_Value__c</formula>
<operation>Formula</operation>
</fieldUpdates>
<rules>
<fullName>Opportunity Copy FMV to Amount</fullName>
<active>false</active>
<booleanFilter>(1 OR 2) AND (3 AND 4)</booleanFilter>
<criteriaItems>
<field>Opportunity.Amount</field>
<operation>equals</operation>
<value>0</value>
</criteriaItems>
<actions>
<name>Opportunity_Copy_FMV_to_Amount</name>
<type>FieldUpdate</type>
</actions>
</rules>
<alerts>
<fullName>Opportunity_Email_Acknowledgment</fullName>
<recipients><field>Primary_Contact__c</field></recipients>
<template>NPSP_.../NPSP_Opportunity_Acknowledgment</template>
</alerts>
</Workflow>
TYPESCRIPT (YOURS) workflows/Opportunity.ts
/** * AUTO-GENERATED by openforce — Workflow Rules for "Opportunity" * → a TypeScript rule engine. * * PERSISTENCE CONTRACT: applyWorkflowRules(record, changeType) is PURE. * Modeled field updates go into a `patch` the caller persists. Every * email alert / task / outbound message is surfaced as a * `pendingManualActions` entry — this module NEVER sends an email, * creates a task, performs a callout, or schedules a delayed action. */ export const WORKFLOW_OBJECT = "Opportunity"; const RULES: RuleDef[] = [ { name: "Opportunity Copy FMV to Amount", active: false, trigger: "onAny", guard: { kind: "unsupported", reason: 'unsupported criteria logic "(1 OR 2) AND (3 AND 4)"' }, actions: [ { name: "Opportunity_Copy_FMV_to_Amount", kind: "fieldUpdate", modeled: true, fieldUpdate: { field: "amount", value: { kind: "formulaRef", fnName: "formula_0_..._Amount" } } }, ], }, // "Opportunity Email Acknowledgment": status field update modeled; // the email alert → pendingManualActions (never sent by the engine); // the Today() date update → flagged (outside the mechanical subset). ];
Source: NPSP workflows/Opportunity.workflow-meta.xml → workflows/Opportunity.ts. The rule above is inactive in the source (active: false) and the engine preserves that faithfully. Its criteria — the (1 OR 2) AND (3 AND 4) boolean filter — is outside the modeled subset, so it fails closed: the rule never fires rather than firing wrong. The email alert and the Today() date update are surfaced for a human, never performed by the engine.
The classic Salesforce pattern: create an Account and a trigger creates its Contact; a trigger on Contact keeps a counter on the Account. Below is the Apex that runs it on Salesforce, the PostgreSQL that runs the same rule after migration, and screenshots of it working live on the migrated demo — plus exactly how your team changes this logic tomorrow.
STATUS: DEPLOYED & PROVEN LIVE · demo.owncrm.dev · CREATE ACCOUNT → COUNT 1 → ADD CONTACT → 2 · BACKFILL 0 DRIFTRead this before the exhibit — we don't blur the line. This trigger chain was hand-written by our engineers in the owned stack; it was not auto-translated from Apex. That is the point, not a caveat: on your own stack the rule is a file your team edits (see “how you change it tomorrow” below).
The runtime capability exists and runs today — the SQL trigger lane is deployed and proven live.
What remains on the roadmap is the automatic translation of an Apex trigger body into that lane.
Until it ships, every Apex trigger is inventoried and fails fast — never silently dropped — and
appears as a named, counted line item in the Org X-Ray, so it is never a surprise on cutover day.
(Most enterprise trigger logic lives in handler classes — NPSP's *_TDTM dispatchers, for
example — and those handler classes are already in the Apex pipeline above.)
Why this is hand-written Apex and not a point-and-click rollup: Contact → Account is a Lookup, and Salesforce Roll-Up Summary fields are only allowed on Master-Detail. On a Lookup the platform gives you nothing declarative — so every team hand-rolls the counter in an Apex trigger, with the full bulkification ceremony (collect ids, one aggregate query, one DML) to survive a 200-record load without tripping a governor limit.
APEX (SALESFORCE) two triggers · one per object
// New Account → create its Primary Contact. Bulk-safe: // build a list, ONE insert for the whole batch. trigger AccountContactTrigger on Account (after insert) { List<Contact> toInsert = new List<Contact>(); for (Account acct : Trigger.new) { toInsert.add(new Contact( FirstName = 'Primary Contact', LastName = acct.Name, // LastName required AccountId = acct.Id, OwnerId = acct.OwnerId)); } if (!toInsert.isEmpty()) insert toInsert; // ^ this insert FIRES the counter trigger — chained. } // Keep the parent's counter in sync. Re-parent touches // BOTH the old and the new Account. trigger ContactCountTrigger on Contact ( after insert, after update, after delete, after undelete) { Set<Id> acctIds = new Set<Id>(); if (Trigger.isInsert || Trigger.isUpdate || Trigger.isUndelete) for (Contact c : Trigger.new) if (c.AccountId != null) acctIds.add(c.AccountId); if (Trigger.isDelete || Trigger.isUpdate) for (Contact c : Trigger.old) if (c.AccountId != null) acctIds.add(c.AccountId); if (acctIds.isEmpty()) return; // ONE aggregate query — never SOQL in a loop. Map<Id,Integer> counts = new Map<Id,Integer>(); for (AggregateResult ar : [SELECT AccountId aid, COUNT(Id) n FROM Contact WHERE AccountId IN :acctIds GROUP BY AccountId]) counts.put((Id) ar.get('aid'), (Integer) ar.get('n')); // ...and ONE DML update back to the parents. List<Account> updates = new List<Account>(); for (Id aid : acctIds) updates.add(new Account(Id = aid, Contact_Count__c = counts.containsKey(aid) ? counts.get(aid) : 0)); update updates; }
POSTGRESQL (YOURS) business-triggers.sql · deployed verbatim
-- Set-based, drift-free recompute. COUNT(*) the actual children -- (never increment a total), after locking the parent so concurrent -- child writes on the same Account serialize. No drift, no lost updates. CREATE OR REPLACE FUNCTION recompute_account_contact_count(p_account_id text) RETURNS void AS $$ BEGIN IF p_account_id IS NULL THEN RETURN; END IF; PERFORM 1 FROM "Account" WHERE id = p_account_id FOR UPDATE; UPDATE "Account" SET "contact_Count" = (SELECT count(*) FROM "Contact" c WHERE c."accountId" = p_account_id), "updatedAt" = now() WHERE id = p_account_id; END; $$ LANGUAGE plpgsql; -- Trigger 1 — AFTER INSERT ON "Account": create the Primary Contact. -- The inserted Contact fires Trigger 2, so the counter lands at 1 -- automatically — chained triggers, exactly like Salesforce. CREATE OR REPLACE FUNCTION account_autocreate_primary_contact() RETURNS trigger AS $$ BEGIN INSERT INTO "Contact" (id, name, "accountId", "ownerId", "createdAt", "updatedAt") VALUES ( 'c' || replace(gen_random_uuid()::text, '-', ''), 'Primary Contact ' || COALESCE(NEW.name, ''), NEW.id, NEW."ownerId", now(), now()); RETURN NULL; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS account_autocreate_primary_contact_trg ON "Account"; CREATE TRIGGER account_autocreate_primary_contact_trg AFTER INSERT ON "Account" FOR EACH ROW EXECUTE FUNCTION account_autocreate_primary_contact(); -- Trigger 2 — maintain the parent's counter on insert / delete / re-parent. CREATE OR REPLACE FUNCTION contact_maintain_account_count() RETURNS trigger AS $$ BEGIN IF TG_OP = 'INSERT' THEN PERFORM recompute_account_contact_count(NEW."accountId"); ELSIF TG_OP = 'DELETE' THEN PERFORM recompute_account_contact_count(OLD."accountId"); ELSIF TG_OP = 'UPDATE' THEN IF OLD."accountId" IS DISTINCT FROM NEW."accountId" THEN PERFORM recompute_account_contact_count(OLD."accountId"); PERFORM recompute_account_contact_count(NEW."accountId"); END IF; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS contact_maintain_account_count_trg ON "Contact"; CREATE TRIGGER contact_maintain_account_count_trg AFTER INSERT OR DELETE OR UPDATE OF "accountId" ON "Contact" FOR EACH ROW EXECUTE FUNCTION contact_maintain_account_count();
The deployed trigger functions and their triggers, verbatim (file comment banner trimmed). Note what is
absent: no batch-size ceremony, no governor-limit dodging, no static recursion guard. The counter is
recomputed from the real rows — a set-based COUNT(*), not an increment — so it can't
drift, and the parent row is locked first, so it stays correct under concurrent writes.
AFTER … UPDATE OF "accountId" means an ordinary field edit never runs the counter path.
Idempotent (CREATE OR REPLACE + DROP TRIGGER IF EXISTS), additive only.
Every screenshot is the real migrated app at demo.owncrm.dev, driven like a user: create the Account “Kestrel Ridge Manufacturing”, then add a second Contact. The counter is maintained by the database.

Create the Account and its Contact related list already shows Contact (1): Primary Contact Kestrel Ridge Manufacturing — auto-created by the Account trigger.

The Account's Contact Count = 1 — the counter trigger fired off the auto-created Contact. Chained triggers, exactly like Salesforce.

Add a second Contact (“Dana Whitfield”) from the related-list New button — Contact (2).

The counter is now Contact Count = 2, with no page-reload logic — the database maintained it.

A pre-existing account (“Amber Peak Energy”) shows Contact Count = 2 — a one-time backfill set the counter for every existing Account from its true child count. Verified 0 drift across all 14 accounts.
Regression check. Opening that same existing account and saving an ordinary edit still works — the
AFTER UPDATE OF "accountId" scoping means a normal field save never runs the counter path.
Old records keep behaving; the new behavior is purely additive.
Safety. A pg_dump backup was taken before any change; the column and triggers are
additive; the only write to existing rows was the counter backfill. Row counts unchanged
(14 accounts / 26 contacts) before and after.
- It's a versioned file, reviewed like any code. The rule lives in a
.sqlfile in your git repo. Changing it is a diff, a pull-request review by your engineer, and one redeploy — no change sets, no sandbox refresh, no waiting on a Salesforce release window. Who changed the rule and why is ingit log. - Want it in TypeScript instead of SQL? Same behavior, different home. The identical rule can live
as a
beforeSave/afterSavehook at the API chokepoint — every write already flows through the generated routes, so a hook there is exactly Salesforce's before/after-trigger contract. The SQL lane (deployed here) and the TypeScript-hook lane are two homes for the same rule; you choose per rule. - The ceremony is gone. No governor limits means no bulkification-for-limits, no
@futureto buy a bigger budget, no static loop-count guard against recursion. The set-based recompute is correct by construction — your engineers write the business rule, not the platform's workarounds.
Business logic as context — not just schema. When we transcribe an org, the free Org Knowledge Map doesn't stop at tables and columns; it reads the automation too. Stated precisely so there's no overclaim, here is what it records for a trigger like this one today:
The trigger file is discovered and parsed into a structured summary — its name, its SOQL queries, its insert/update/delete operations. Every field the trigger's SOQL touches is marked “referenced by automation” and scored as actively used — not dead metadata, so a counter field maintained by a trigger shows up as load-bearing, not orphaned. An opt-in, key-gated inference pass writes the human-readable purpose of each element and raises confidence when multiple automations act on it.
Honest boundary: today the map records that the automation exists and which fields it acts on (a best-effort SOQL scan, documented as not-yet-a-full-parse). It does not yet store the rule's complete semantics as structured data — that's the same trigger-translation surface still on the roadmap. Either way, your CRM carries its behavior as context next to its shape, so the next system — human or AI — knows the rule is there, not just the columns.
Five statuses appear across this page and in every Org X-Ray. Counts shown are from the NPSP batch.
| Status | What it means | On NPSP |
|---|---|---|
| Converted | Deterministic — mechanical transforms only, no AI touched it. | 51 |
| Converted & verified | AI-assisted, then passed our machine gates: strict re-compile, pinned SQL used verbatim, code-safety scan. | 31 |
| Draft | A translation exists and compiles, but the engine flagged the exact lines and reasons a human must confirm. Our engineers execute this list — a person in the lead, AI as leverage. | 546 |
| Redone by hand | Failed our own checks. We say so, and rebuild it by hand rather than ship something unverified. | 4 |
| Queued | A typed stub and parity harness are in place, awaiting the translation batch. | 45 |
Sources & batch verdict. Every figure is engine output.
This class: coral-cloud/…/ListExperiencesFromType.cls → translated-coral/ListExperiencesFromType.ts (.meta.json: 0 flags).
Coral batch: 14/14 real-logic classes processed — 1 verified · 13 drafts · 0 rejected.
NPSP batch: 603 translations re-verified → 31 LLM-verified + 51 deterministic promoted canonical, 546 drafts (flagged, line-referenced), 4 rejected, and 45 scaffolds still awaiting translation. Every promotion earns the word verified; every draft carries its line-referenced reason.