Review and publish rule changes
Outcome
A rule version moves through DRAFT → IN_REVIEW → PUBLISHED via the Rules Engine Editor with a complete audit trail. The change resolves correctly in production for the intended scope.
Prerequisites
rules.readto browse,rules.updateto author and transition.- A rule kind in mind: INGEST, CHARGE_VALIDATE, CHARGE_DERIVE, CLAIM_BUILD_GROUPING, PRE_SUBMIT_VALIDATE, COMPANION_GUIDE_VALIDATE, POSTING, EVV_CONFIG.
Lifecycle
There is no separate APPROVED state on the server; approval is the verb, PUBLISHED is the terminal pre-archive status.
Steps — primary path (UI)
Open the editor:
Configuration → Rules → /admin/rules. Pick a kind from the sidebar.Open the rule set's version list by clicking a row. Hit New draft to open the Monaco YAML editor seeded with a schema-valid template.
Edit the YAML. Live lint runs against the kind's Zod schema; the header shows
✓ schema validor an error count. Save as draft is disabled until the schema is green.Optionally bind the new version to a narrower scope via the Scope picker (facility × site × billing entity × payer × program × service-line × effective date). Click Bind scope before Save as draft so the scope threads through.
Use the Dry-run panel to paste a sample entity and confirm precedence
- rule resolution before promoting the draft.
Submit for review to move DRAFT → IN_REVIEW. An approver (anyone with
rules.update) clicks Approve & publish (IN_REVIEW → PUBLISHED) or Reject (requires a reason; sends it back to DRAFT).Once PUBLISHED, the Version diff view shows side-by-side changes vs. the prior PUBLISHED version, and the Approval history drawer captures the SUBMIT / APPROVE / REJECT / ARCHIVE trail.
Archive the old version when superseding: open the old row, click Archive (PUBLISHED → ARCHIVED). New evaluations stop resolving to it immediately.
E2E coverage: tests/e2e/specs/rules-engine.spec.ts.
Validation
| Check | Expected |
|---|---|
| Editor schema badge | ✓ schema valid before save |
| Version row | DRAFT → IN_REVIEW → PUBLISHED transitions visible |
| Approval history drawer | All transitions captured with actor + reason |
| Resolver | /resolve returns the new version for the bound scope |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Save as draft disabled | Schema validation failing | Read the editor's error count + Monaco markers. |
| Reject button missing in IN_REVIEW | Reviewer's role lacks rules.update | Escalate scope. |
| Diff view shows nothing | Prior PUBLISHED version doesn't exist (this is the first published) | Expected. |
| Resolver still returns the old version | Cache or scope mismatch | Confirm config_scope matches; some resolvers cache for ~30s. |
Break-glass: API / SQL
Use only when the UI is unavailable (e.g. rcm-app down, rcm-core up).
# Create artifact
curl -X POST http://localhost:3008/artifacts \
-H "Content-Type: application/json" \
-d '{"content": "<yaml rule content>", "contentType": "application/yaml"}'
# Create version pointing at the artifact (DRAFT)
curl -X POST http://localhost:3008/versions \
-H "Content-Type: application/json" \
-d '{"ruleSetId":"<id>","artifactHash":"<hash>",
"effectiveFrom":"2026-01-01","lifecycleStatus":"DRAFT"}'
Submit for review:
UPDATE config.rule_set_version
SET lifecycle_status = 'IN_REVIEW'
WHERE version_id = '<id>' AND lifecycle_status = 'DRAFT';
INSERT INTO config.config_version_approval (version_id, action,
performed_by, comments)
VALUES ('<id>', 'SUBMIT', 'user@example.com', 'Ready for review');
Approve and publish:
INSERT INTO config.config_version_approval (version_id, action,
performed_by, comments)
VALUES ('<id>', 'APPROVE', 'reviewer@example.com', 'Approved');
UPDATE config.rule_set_version
SET lifecycle_status = 'PUBLISHED',
published_by = 'reviewer@example.com', published_at = NOW()
WHERE version_id = '<id>' AND lifecycle_status = 'IN_REVIEW';
Cross-references
- Modifier rules editing — modifier validation rule sets deep-link from there to UI-01.
- State configuration navigator for state-scoped rule sets.