Docs
GitHub

Write a governance rule

A rule is a strict Zod schema plus a cited policy check, living in @sina-design-system/fintech. The boundary is hard: pure Zod, no React, no UI, with the shared contract coming from @sina-design-system/governance. This guide mirrors the /new-schema skill; to see a finished rule at work, read the $60k wire.

Never invent a limit

Every threshold, format, and required field must trace to a standard or regulator, with the citation in the code: a // 31 CFR … comment on the constant and standard: "…" on the violation. If you can't cite it, don't encode it. Citing also marks which numbers are law and which are tunable product defaults.

Define the schema

Create packages/fintech/src/<name>/<name>.schema.ts, modeled on the wire-transfer schema. Use a .strict() Zod object so smuggled keys are rejected rather than ignored, and build on the cited format primitives in src/formats/*: currency (ISO 4217), amount, iban, bic, routing, card. Amounts are integer minor units, never floats.

Add the policy check

Each policy check (limits, velocity, dual control) produces a Violation with a code, a message, a cited standard, and a severity: flag (record it), escalate (force a governed component), or reject (refuse outright); see Escalation. An escalation names the requiredComponent. The rule must return the contract:

// pass
{ valid: true, violations: [], requiredComponent: null }
// fail
{ valid: false, violations: [{ code, message, standard }], requiredComponent: "SecureWireDialog" }

An ungoverned display rule is the same skeleton minus policy and escalation: shape-only (.strict(), bounded arrays, masked account numbers), mounting a presentational component. Use /new-display-pattern for that.

Register the intent

Add the verb to src/registry.ts (and fintechIntentManifest()) so evaluateFintechIntent can route to it. Unregistered verbs are denied as UNKNOWN_INTENT; a rule that isn't registered doesn't exist.

Write the fixtures

Add fixtures.ts beside the schema: valid payloads that must pass and adversarial payloads that must be blocked, such as an over-limit amount, a smuggled extra field, or an unmasked identifier. Both the unit tests and the playground consume them. A rule isn't done when it compiles; it's done when its hostile fixtures can't get past it.

Test and verify

Run pnpm --filter @sina-design-system/fintech test, then exercise the rule and read the decision (evaluateFintechIntent returns the decision envelope; the contract lives on result):

const decision = evaluateFintechIntent({
  intent: "wire_transfer",
  props: { amount: 6000000 /* $60,000 */, currency: "USD", /* … */ },
});

decision.result.valid;             // false
decision.result.requiredComponent; // "SecureWireDialog"
decision.mount;                    // "SecureWireDialog"

Each violation carries a message and a standard, so you can show the user why, with a citation. Lower the amount below the threshold and re-run: valid flips to true and requiredComponent is null.

Add your own currency bands

The stock bands are USD only; a non-USD payload passes format checks and carries a POLICY_BANDS_NOT_EVALUATED flag instead of silently passing. To band another currency, pass bands (an array of WireBand) on the context accepted by makeWirePolicy and evaluateWireTransfer, so the secondary-approval band runs at your own par. Reuse the exported approvalViolations for the dual-control core (payload binding plus separation of duties) rather than reimplementing it, and cite the deviation in each violation's standard field.

Before you ship, check that you can answer in one sentence each: what the rule is, the risk it removes, a request it blocks, a request it passes, and what happens on trigger.

Next: Governed components & display patterns, where your escalation lands.