@open-mercato/cli 0.6.8-develop.6964.1.36b364cfd8 → 0.6.8-develop.6971.1.20c09ca9ea

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/agentic/claude-code/hooks/gate-evidence.ts +269 -0
  2. package/dist/agentic/claude-code/settings.json +21 -0
  3. package/dist/agentic/codex/hooks/gate-evidence.mjs +199 -0
  4. package/dist/agentic/codex/hooks.json +29 -0
  5. package/dist/agentic/cursor/hooks/gate-evidence.mjs +181 -0
  6. package/dist/agentic/cursor/hooks.json +10 -0
  7. package/dist/agentic/guides/framework-contracts.md +82 -0
  8. package/dist/agentic/guides/module-facts.json +112 -112
  9. package/dist/agentic/guides/module-facts.v2.json +112 -112
  10. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  11. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  12. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  13. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  14. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  15. package/dist/agentic/guides/modules/auth/index.md +1 -1
  16. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  17. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  18. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  19. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  20. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  21. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  22. package/dist/agentic/guides/modules/configs/index.md +1 -1
  23. package/dist/agentic/guides/modules/content/index.md +1 -1
  24. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  25. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  26. package/dist/agentic/guides/modules/customers/index.md +1 -1
  27. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  28. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  29. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  30. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  31. package/dist/agentic/guides/modules/directory/index.md +1 -1
  32. package/dist/agentic/guides/modules/entities/index.md +1 -1
  33. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  34. package/dist/agentic/guides/modules/events/index.md +1 -1
  35. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  36. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  37. package/dist/agentic/guides/modules/generators/index.md +1 -1
  38. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  39. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  40. package/dist/agentic/guides/modules/messages/index.md +1 -1
  41. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  42. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  43. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  44. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  45. package/dist/agentic/guides/modules/planner/index.md +1 -1
  46. package/dist/agentic/guides/modules/portal/index.md +1 -1
  47. package/dist/agentic/guides/modules/progress/index.md +1 -1
  48. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  49. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  50. package/dist/agentic/guides/modules/resources/index.md +1 -1
  51. package/dist/agentic/guides/modules/sales/index.md +1 -1
  52. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  53. package/dist/agentic/guides/modules/search/index.md +1 -1
  54. package/dist/agentic/guides/modules/security/index.md +1 -1
  55. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  56. package/dist/agentic/guides/modules/sso/index.md +1 -1
  57. package/dist/agentic/guides/modules/staff/index.md +1 -1
  58. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  59. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  60. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  61. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  62. package/dist/agentic/guides/modules/translations/index.md +1 -1
  63. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  64. package/dist/agentic/guides/modules/wms/index.md +1 -1
  65. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  66. package/dist/agentic/guides/reference-module-facts.json +1 -1
  67. package/dist/agentic/guides/upstream/manifest.json +1 -1
  68. package/dist/agentic/shared/AGENTS.md.template +3 -3
  69. package/dist/agentic/shared/ai/agentic.config.json +1 -0
  70. package/dist/agentic/shared/ai/harness/README.md +4 -4
  71. package/dist/agentic/shared/ai/harness/RELEASE.md +4 -4
  72. package/dist/agentic/shared/ai/harness/cases.json +93 -14
  73. package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
  74. package/dist/agentic/shared/ai/harness/fixtures/index.json +2 -1
  75. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +5 -0
  76. package/dist/agentic/shared/ai/harness/release-matrix.json +5 -4
  77. package/dist/agentic/shared/ai/harness/source-link-inventory.json +196 -4
  78. package/dist/agentic/shared/ai/harness/validators.json +4 -3
  79. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +9 -1
  80. package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +72 -4
  81. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +2 -0
  82. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +1 -1
  83. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +1 -1
  84. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +2 -1
  85. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +13 -1
  86. package/dist/agentic/shared/ai/skills/om-implement-spec/references/resume.md +17 -0
  87. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +3 -0
  88. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +2 -0
  89. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +1 -0
  90. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +13 -0
  91. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +22 -0
  92. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +20 -0
  93. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +4 -0
  94. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +36 -1
  95. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +61 -8
  96. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1 -1
  97. package/dist/lib/agentic-setup.js +8 -2
  98. package/dist/lib/agentic-setup.js.map +2 -2
  99. package/dist/lib/generators/module-di.js +25 -1
  100. package/dist/lib/generators/module-di.js.map +2 -2
  101. package/dist/lib/generators/module-registry.js +61 -15
  102. package/dist/lib/generators/module-registry.js.map +2 -2
  103. package/dist/lib/generators/openapi.js +3 -1
  104. package/dist/lib/generators/openapi.js.map +2 -2
  105. package/package.json +6 -6
  106. package/src/lib/agentic-setup.ts +17 -2
  107. package/src/lib/generators/__tests__/silent-convention-warnings.test.ts +154 -0
  108. package/src/lib/generators/module-di.ts +35 -0
  109. package/src/lib/generators/module-registry.ts +93 -7
  110. package/src/lib/generators/openapi.ts +5 -1
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Record validation-gate outcomes, and refuse to finish on unverified source changes (Cursor).
3
+ *
4
+ * Mirrors the claude-code hook: `record` on `afterShellExecution`, `check` on `stop`.
5
+ *
6
+ * Cursor's `afterShellExecution` payload documents `command`, `output`, `duration`, and
7
+ * `sandbox` — no exit code — so the outcome is inferred from output text rather than read.
8
+ * Blocking contract: exit 2 blocks, exit 0 proceeds, other codes fail open.
9
+ */
10
+ import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
11
+ import { join, resolve } from 'node:path'
12
+
13
+ /**
14
+ * Extracts every gate a shell command ran.
15
+ *
16
+ * Quoted spans are blanked first: `git commit -m "run tsc --noEmit"` names a gate without
17
+ * running one, and its zero exit status would otherwise be recorded as a green typecheck.
18
+ */
19
+ export function matchGates(command) {
20
+ const executable = command.replace(/'[^']*'|"[^"]*"/g, ' ')
21
+ const found = new Set()
22
+ const named = [
23
+ ['typecheck', /\b(?:yarn|npm run|pnpm)\s+typecheck\b|\btsc\b[^&|;]*--noEmit/],
24
+ ['lint', /\b(?:yarn|npm run|pnpm)\s+lint\b|\beslint\b/],
25
+ ['test', /\b(?:yarn|npm run|pnpm)\s+test\b|\bjest\b/],
26
+ ['build', /\b(?:yarn|npm run|pnpm)\s+build\b|\bnext build\b/],
27
+ ['generate', /\b(?:yarn|npm run|pnpm)\s+generate\b|\bmercato\s+generate\b/],
28
+ ]
29
+ for (const [gate, pattern] of named) {
30
+ if (pattern.test(executable)) found.add(gate)
31
+ }
32
+ return [...found]
33
+ }
34
+
35
+ /**
36
+ * Whether a command's outcome can be attributed to the gates it names.
37
+ *
38
+ * A pipeline reports its LAST stage's status, and `;` / `||` break the link the same way.
39
+ * `&&` does not: it short-circuits, so a failure still belongs to a gate that ran.
40
+ */
41
+ export function isAttributableGateCommand(command) {
42
+ return !/[|;\n]/.test(command)
43
+ }
44
+
45
+ /** See the claude-code hook. */
46
+ export function nextSessionState(previous, sessionId, startedAt) {
47
+ if (!sessionId) {
48
+ return previous.sessionStartedAt ? previous : { ...previous, sessionStartedAt: startedAt }
49
+ }
50
+ if (previous.sessionId === sessionId && previous.sessionStartedAt) return previous
51
+ return { sessionId, sessionStartedAt: startedAt }
52
+ }
53
+
54
+ /** See the claude-code hook: an absent typecheck record does not block on its own. */
55
+ export function shouldBlock({ newestSrcMtimeMs, sessionStartedAtMs, lastGreenTypecheckMs }) {
56
+ if (newestSrcMtimeMs === null) return false
57
+ if (newestSrcMtimeMs < sessionStartedAtMs) return false
58
+ if (lastGreenTypecheckMs === null) return true
59
+ return newestSrcMtimeMs > lastGreenTypecheckMs
60
+ }
61
+
62
+ /**
63
+ * Infers a gate outcome from its output, because this host reports no exit code.
64
+ *
65
+ * Returns 1 (failure) for empty output and for any failure signature. The bias is
66
+ * deliberate: over-reporting failure costs one re-run, under-reporting it records a pass
67
+ * that never happened — the property this hook exists to remove. `No tests found` is a
68
+ * failure here for the same reason it is not a pass in the harness rules.
69
+ */
70
+ export function inferExitCode(output) {
71
+ if (typeof output !== 'string' || output.trim() === '') return 1
72
+ const failureSignatures = [
73
+ /\berror\s+TS\d+/i, /FATAL ERROR/i, /heap out of memory/i,
74
+ /\bTests?:\s+\d+\s+failed/i, /\bfail(ed|ing)\b/i, /\bERROR\b/,
75
+ /exited \(\d+\)/, /command not found/i, /No tests found/i,
76
+ ]
77
+ return failureSignatures.some((pattern) => pattern.test(output)) ? 1 : 0
78
+ }
79
+
80
+ const STATE_RELATIVE_PATH = '.ai/.gate-state.json'
81
+ const WATCHED_ROOT = 'src'
82
+
83
+ function projectDir() {
84
+ return process.env.CURSOR_PROJECT_DIR || resolve('.')
85
+ }
86
+
87
+ function statePath() { return join(projectDir(), STATE_RELATIVE_PATH) }
88
+
89
+ function readState() {
90
+ try { return JSON.parse(readFileSync(statePath(), 'utf8')) } catch { return {} }
91
+ }
92
+
93
+ function writeState(state) {
94
+ try {
95
+ mkdirSync(join(projectDir(), '.ai'), { recursive: true })
96
+ writeFileSync(statePath(), `${JSON.stringify(state, null, 2)}\n`, 'utf8')
97
+ } catch {
98
+ // A hook must never fail the turn over its own bookkeeping.
99
+ }
100
+ }
101
+
102
+ function newestMtimeMs(dir) {
103
+ let newest = null
104
+ const walk = (current) => {
105
+ let entries
106
+ try { entries = readdirSync(current) } catch { return }
107
+ for (const entry of entries) {
108
+ if (entry === 'node_modules' || entry.startsWith('.')) continue
109
+ const full = join(current, entry)
110
+ let stats
111
+ try { stats = statSync(full) } catch { continue }
112
+ if (stats.isDirectory()) walk(full)
113
+ else if (newest === null || stats.mtimeMs > newest) newest = stats.mtimeMs
114
+ }
115
+ }
116
+ walk(dir)
117
+ return newest
118
+ }
119
+
120
+ function readStdin() {
121
+ return new Promise((done) => {
122
+ let raw = ''
123
+ if (process.stdin.isTTY) { done(''); return }
124
+ process.stdin.setEncoding('utf8')
125
+ process.stdin.on('data', (chunk) => { raw += chunk })
126
+ process.stdin.on('end', () => done(raw))
127
+ process.stdin.on('error', () => done(''))
128
+ })
129
+ }
130
+
131
+ async function main() {
132
+ const mode = process.argv[2] === 'check' ? 'check' : 'record'
133
+ const raw = await readStdin()
134
+
135
+ let data = {}
136
+ if (raw.trim()) {
137
+ try { data = JSON.parse(raw) } catch { process.exit(0) }
138
+ }
139
+
140
+ const previous = readState()
141
+ const now = new Date()
142
+ const state = nextSessionState(previous, data.session_id ?? null, now.toISOString())
143
+ if (state !== previous) writeState(state)
144
+
145
+ if (mode === 'record') {
146
+ const command = data.command ?? data.tool_input?.command
147
+ if (!command) process.exit(0)
148
+ const gates = matchGates(command)
149
+ if (!gates.length) process.exit(0)
150
+ if (!isAttributableGateCommand(command)) process.exit(0)
151
+ const exitCode = inferExitCode(data.output ?? '')
152
+ state.gates = state.gates ?? {}
153
+ for (const gate of gates) {
154
+ state.gates[gate] = { exitCode, finishedAt: now.toISOString() }
155
+ }
156
+ writeState(state)
157
+ process.exit(0)
158
+ }
159
+
160
+ if (data.stop_hook_active) process.exit(0)
161
+
162
+ const typecheck = state.gates?.typecheck
163
+ const blocked = shouldBlock({
164
+ newestSrcMtimeMs: newestMtimeMs(join(projectDir(), WATCHED_ROOT)),
165
+ sessionStartedAtMs: Date.parse(state.sessionStartedAt ?? now.toISOString()),
166
+ lastGreenTypecheckMs: typecheck && typecheck.exitCode === 0 ? Date.parse(typecheck.finishedAt) : null,
167
+ })
168
+ if (!blocked) process.exit(0)
169
+
170
+ process.stderr.write([
171
+ `Source under ${WATCHED_ROOT}/ changed this session and has not passed a typecheck since.`,
172
+ '',
173
+ 'Run `yarn typecheck` and report its exit status before finishing.',
174
+ 'If it genuinely fails and you cannot fix it, report the failure to the user.',
175
+ '',
176
+ ].join('\n'))
177
+ process.exit(2)
178
+ }
179
+
180
+ const invokedDirectly = process.argv[1] && process.argv[1].endsWith('gate-evidence.mjs')
181
+ if (invokedDirectly) await main()
@@ -5,6 +5,16 @@
5
5
  "command": "node",
6
6
  "args": [".cursor/hooks/entity-migration-check.mjs"],
7
7
  "description": "Check if entity edit requires a database migration"
8
+ },
9
+ "afterShellExecution": {
10
+ "command": "node",
11
+ "args": [".cursor/hooks/gate-evidence.mjs", "record"],
12
+ "description": "Record validation-gate outcomes so a gate cannot be claimed without running"
13
+ },
14
+ "stop": {
15
+ "command": "node",
16
+ "args": [".cursor/hooks/gate-evidence.mjs", "check"],
17
+ "description": "Block finishing when src/ changed without a passing typecheck since"
8
18
  }
9
19
  }
10
20
  }
@@ -0,0 +1,82 @@
1
+ # Installed Framework Contracts
2
+
3
+ Use this digest after app call sites and generated facts identify a named framework contract. Links are read-only version evidence: never edit or enumerate `node_modules`. If a question is unanswered, invoke `om-framework-context` for only that contract.
4
+
5
+ ## Command handler and registry
6
+
7
+ [`CommandHandler`](../../node_modules/@open-mercato/shared/src/lib/commands/types.ts) has a stable `id` and one required operation, `execute(input, ctx)`. Its optional lifecycle is deliberately split:
8
+
9
+ - `prepare(input, ctx)` captures the before snapshot before execution.
10
+ - `execute(input, ctx)` performs the durable operation and returns the public result.
11
+ - `captureAfter(input, result, ctx)` can capture the after snapshot.
12
+ - `buildLog({ input, result, ctx, snapshots })` returns action-log metadata; returning `null` or `skipLog` suppresses a normal log entry.
13
+ - `undo({ input, ctx, logEntry })` and `redo(...)` are only valid when the handler owns a reversible contract. Undo payload is stored under `commandPayload`, not `logEntry.payload`; use the shared undo extractor.
14
+
15
+ `CommandRuntimeContext` carries authentication, selected organization scope, the DI container, optional request and sync origin, bulk-import suppression, and an optional `transactionalEm`. A handler must preserve this scope and reuse `transactionalEm` when supplied instead of opening an independent transaction.
16
+
17
+ [The command registry and `registerCommand`](../../node_modules/@open-mercato/shared/src/lib/commands/registry.ts) own handler discovery. Register through the normal `commands.ts` discovery surface. IDs are global and stable: duplicates throw outside development; development permits HMR replacement. Never silently replace a production ID or call registry internals from a module.
18
+
19
+ ## Atomic CRUD command writes
20
+
21
+ [`runCrudCommandWrite`](../../node_modules/@open-mercato/shared/src/lib/commands/runCrudCommandWrite.ts) is the command-owned persistence seam for CRUD-backed handlers. It:
22
+
23
+ It resolves or reuses an entity manager; passes all phases through transactional-by-default `withAtomicFlush`; applies custom fields after durable phases; and emits configured CRUD/index side effects from `sideEffect()`.
24
+
25
+ Every phase must use the callback's entity manager. Set `transaction: false` only when an enclosing transaction already provides the atomic boundary. `sideEffect()` must return the final entity and stable identifiers after phases have completed. Commands using this helper own generic event/reindex emission; their route must not repeat it.
26
+
27
+ ## `makeCrudRoute`
28
+
29
+ [`makeCrudRoute`](../../node_modules/@open-mercato/shared/src/lib/crud/factory.ts) is the installed HTTP factory. Its `metadata.GET/POST/PUT/DELETE` gates authentication, immutable feature requirements, and rate limits per method. The factory also retains tenant/organization selection checks, interceptors, mutation guards, synchronous before/after lifecycle subscribers, response shaping, command operation headers, and errors. Command-backed actions extend that envelope; they do not replace it.
30
+
31
+ ORM defaults are `id`, `organizationId`, `tenantId`, and `deletedAt`; `null` disables an automatic field. Do not disable scope merely because a handler checks it too. With the specialized read-only `omitAutomaticTenantOrgScope`, `buildFilters` must encode all visibility and fail closed; update/delete scoping remains automatic.
32
+
33
+ ### List sequence
34
+
35
+ - The list zod schema parses query parameters; before interceptors may rewrite it, followed by another parse.
36
+ - `hooks.beforeList(validatedQuery, ctx)` runs after rewriting and before the query. Resolve request-specific state through `ctx`, never module-global mutable state.
37
+ - `list.transformItem(item)` is a synchronous per-row projection. On the Query Engine path it runs before custom-field decoration, translation overlay, and response enrichment. It must not perform async I/O.
38
+ - `hooks.afterList(payload, { ...ctx, query })` receives the mutable response payload on cache, query-engine, ORM-fallback, export, and empty-result branches. Response enrichers configured by `enrichers.entityId` run after `afterList`; after interceptors also remain part of the factory response pipeline.
39
+
40
+ Keep entity/enricher IDs and list fields, filters, sorting, joins, and export settings stable. `buildFilters` may be async; `transformItem` may not. Query Engine options do not apply when the route omits `entityId` or `fields`.
41
+
42
+ ### Command-backed mutations
43
+
44
+ `actions.create`, `actions.update`, and `actions.delete` select the command path by stable `commandId`. An action may map validated input, add log metadata, choose response/status, and provide resource identity. The command performs persistence and asynchronous generic CRUD side effects. The factory still performs:
45
+
46
+ - route authentication and selected-scope rejection;
47
+ - request interceptors and schema validation;
48
+ - synchronous lifecycle events and mutation guards;
49
+ - command-bus execution and action-log operation headers;
50
+ - response interceptors, guard success callbacks, and cache invalidation.
51
+
52
+ Thus commands own the asynchronous configured CRUD event/index work, not the factory's synchronous lifecycle. The factory deliberately does not double-emit the command's generic event.
53
+
54
+ ## Typed module events
55
+
56
+ [`createModuleEvents`](../../node_modules/@open-mercato/shared/src/modules/events/factory.ts) adds `module` to definitions, registers IDs for runtime validation, and returns typed `eventsConfig.emit`. The emitted ID must be declared by that configuration.
57
+
58
+ `eventsConfig.emit(eventId, payload, options)` delegates immediately to the bootstrapped event bus. It does not begin, join, or wait for a database transaction. Calling it before commit can publish an event for a write that later rolls back. Emit by call order only after the durable write succeeds, or use the post-write flow supplied by `runCrudCommandWrite`/the Data Engine. `clientBroadcast` and `portalBroadcast` are declaration flags, not transaction controls.
59
+
60
+ ## Optimistic locking
61
+
62
+ [Command optimistic-lock helpers](../../node_modules/@open-mercato/shared/src/lib/crud/optimistic-lock-command.ts) normalize an explicit expected version or request header and compare it with actual `updated_at`. `assertOptimisticLock` does nothing when the token is absent or locking is disabled, and throws the shared 409 conflict on mismatch.
63
+
64
+ `expected_updated_at` is an app convention, not inferred. Validate and pass it as `expected`; load and assert inside the mutation transaction. Return `updated_at`/`updatedAt` so the UI can send the version.
65
+
66
+ ## Defensive JSON reads
67
+
68
+ [`readJsonSafe`](../../node_modules/@open-mercato/shared/src/lib/http/readJsonSafe.ts) accepts a `Request`, `Response`, or string and returns the supplied fallback for empty/invalid JSON. It is not schema validation: narrow or zod-parse its result.
69
+
70
+ ## Data Engine ORM operations
71
+
72
+ [The Data Engine](../../node_modules/@open-mercato/shared/src/lib/data/engine.ts) provides `createOrmEntity`, `updateOrmEntity`, and `deleteOrmEntity` for generic ORM-backed mutation seams:
73
+
74
+ - `createOrmEntity({ entity, data })` creates, persists, and flushes, then returns the entity.
75
+ - `updateOrmEntity({ entity, where, apply })` loads one matching entity, returns `null` when absent, applies the callback, flushes, and returns the entity.
76
+ - `deleteOrmEntity({ entity, where, soft, softDeleteField })` loads one matching entity, returns `null` when absent, then soft-deletes or removes and flushes.
77
+
78
+ These helpers do not derive tenant/organization scope or automatically emit. The caller owns complete `where` scope and create fields. `markOrmEntityChange` queues event/index work, `flushOrmEntityChanges` drains it, and `emitOrmEntityEvent` emits directly. Drain only after the durable write; suppressed indexing must be rebuilt.
79
+
80
+ ## Escalation rule
81
+
82
+ Start here, then the one exact linked file. If unanswered, invoke `om-framework-context` with a narrow query and report the installed version and limitation. Never copy a private implementation detail into an app contract.