@hotmeshio/long-tail 0.10.1 → 0.10.3
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.
- package/README.md +1 -0
- package/build/api/bot-accounts.d.ts +4 -0
- package/build/api/bot-accounts.js +3 -1
- package/build/api/escalations/metadata.js +21 -1
- package/build/api/escalations/resolve.d.ts +8 -0
- package/build/api/escalations/resolve.js +82 -3
- package/build/api/me.d.ts +16 -0
- package/build/api/me.js +79 -0
- package/build/api/roles.d.ts +8 -0
- package/build/api/roles.js +14 -0
- package/build/bin/ltc.js +10 -1
- package/build/lib/cli/client.d.ts +4 -1
- package/build/lib/cli/client.js +4 -2
- package/build/lib/db/schemas/022_user_preferences.sql +18 -0
- package/build/lib/db/schemas/023_role_enforce_schema.sql +18 -0
- package/build/lib/typed-resolution.d.ts +35 -0
- package/build/lib/typed-resolution.js +37 -0
- package/build/modules/config.d.ts +1 -0
- package/build/modules/config.js +5 -0
- package/build/routes/bot-accounts.js +3 -1
- package/build/routes/index.js +2 -0
- package/build/routes/me.d.ts +2 -0
- package/build/routes/me.js +57 -0
- package/build/sdk/index.d.ts +10 -0
- package/build/sdk/index.js +25 -0
- package/build/services/escalation/crud.d.ts +18 -6
- package/build/services/escalation/crud.js +18 -2
- package/build/services/escalation/index.d.ts +1 -0
- package/build/services/escalation/index.js +1 -0
- package/build/services/escalation/resolver-validation.d.ts +28 -0
- package/build/services/escalation/resolver-validation.js +95 -0
- package/build/services/escalation/sql.d.ts +12 -2
- package/build/services/escalation/sql.js +22 -2
- package/build/services/iam/bots.d.ts +4 -1
- package/build/services/iam/bots.js +5 -3
- package/build/services/iam/sql.d.ts +2 -2
- package/build/services/iam/sql.js +15 -1
- package/build/services/role/enforcement-cache.d.ts +21 -0
- package/build/services/role/enforcement-cache.js +0 -0
- package/build/services/role/index.js +6 -0
- package/build/services/role/sql.d.ts +15 -2
- package/build/services/role/sql.js +21 -2
- package/build/services/role/types.d.ts +25 -0
- package/build/services/user/index.d.ts +1 -0
- package/build/services/user/index.js +5 -1
- package/build/services/user/preferences.d.ts +15 -0
- package/build/services/user/preferences.js +38 -0
- package/build/shared/form-validation/field-validator.d.ts +45 -0
- package/build/shared/form-validation/field-validator.js +214 -0
- package/build/shared/form-validation/index.d.ts +11 -0
- package/build/shared/form-validation/index.js +27 -0
- package/build/shared/form-validation/validate-resolver-payload.d.ts +41 -0
- package/build/shared/form-validation/validate-resolver-payload.js +62 -0
- package/build/shared/form-validation/x-lt-bind.d.ts +41 -0
- package/build/shared/form-validation/x-lt-bind.js +110 -0
- package/build/shared/form-validation/x-lt-help.d.ts +21 -0
- package/build/shared/form-validation/x-lt-help.js +91 -0
- package/build/shared/form-validation/x-lt-show-if.d.ts +7 -0
- package/build/shared/form-validation/x-lt-show-if.js +84 -0
- package/build/system/mcp-servers/admin/schemas.d.ts +29 -0
- package/build/system/mcp-servers/admin/schemas.js +7 -0
- package/build/system/mcp-servers/admin/users.js +8 -3
- package/build/system/mcp-servers/human-queue-schemas.d.ts +4 -4
- package/build/system/mcp-servers/human-queue-schemas.js +4 -1
- package/build/system/mcp-servers/human-queue.js +41 -2
- package/build/system/seed/tool-manifests-admin.d.ts +167 -0
- package/build/system/seed/tool-manifests-admin.js +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/sdk.d.ts +6 -1
- package/build/types/validation.d.ts +43 -0
- package/build/types/validation.js +28 -0
- package/dashboard/dist/assets/{AdminDashboard-D7Amh-EJ.js → AdminDashboard-DQLGybZ9.js} +2 -2
- package/dashboard/dist/assets/{AdminDashboard-D7Amh-EJ.js.map → AdminDashboard-DQLGybZ9.js.map} +1 -1
- package/dashboard/dist/assets/AgentConfigPage-4JgMpuYJ.js +16 -0
- package/dashboard/dist/assets/AgentConfigPage-4JgMpuYJ.js.map +1 -0
- package/dashboard/dist/assets/{AgentDetailPage-Dy3Qvlfk.js → AgentDetailPage-DCgKokgX.js} +3 -3
- package/dashboard/dist/assets/{AgentDetailPage-Dy3Qvlfk.js.map → AgentDetailPage-DCgKokgX.js.map} +1 -1
- package/dashboard/dist/assets/{AgentsPage-D7WePZ0o.js → AgentsPage-BK_8VKZO.js} +2 -2
- package/dashboard/dist/assets/{AgentsPage-D7WePZ0o.js.map → AgentsPage-BK_8VKZO.js.map} +1 -1
- package/dashboard/dist/assets/AvailableEscalationsPage-Dp1rsIHC.js +2 -0
- package/dashboard/dist/assets/AvailableEscalationsPage-Dp1rsIHC.js.map +1 -0
- package/dashboard/dist/assets/BotPicker-DvUnYEJk.js +2 -0
- package/dashboard/dist/assets/BotPicker-DvUnYEJk.js.map +1 -0
- package/dashboard/dist/assets/{CapabilitiesPage-3VTLEPGO.js → CapabilitiesPage-CJ5ldY8w.js} +2 -2
- package/dashboard/dist/assets/{CapabilitiesPage-3VTLEPGO.js.map → CapabilitiesPage-CJ5ldY8w.js.map} +1 -1
- package/dashboard/dist/assets/{CollapsibleSection-BYgeZz9M.js → CollapsibleSection-BXOpfs3z.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-BYgeZz9M.js.map → CollapsibleSection-BXOpfs3z.js.map} +1 -1
- package/dashboard/dist/assets/CountdownTimer-D20FNwS-.js +2 -0
- package/dashboard/dist/assets/CountdownTimer-D20FNwS-.js.map +1 -0
- package/dashboard/dist/assets/{CredentialsPage-CaH3J3SE.js → CredentialsPage-vZ2psNnd.js} +2 -2
- package/dashboard/dist/assets/{CredentialsPage-CaH3J3SE.js.map → CredentialsPage-vZ2psNnd.js.map} +1 -1
- package/dashboard/dist/assets/{CronLabel-Bma_4AC2.js → CronLabel-KRkqNSYs.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-Bma_4AC2.js.map → CronLabel-KRkqNSYs.js.map} +1 -1
- package/dashboard/dist/assets/{CustomDurationPicker-BxyjEX27.js → CustomDurationPicker-C-vGsrFl.js} +2 -2
- package/dashboard/dist/assets/CustomDurationPicker-C-vGsrFl.js.map +1 -0
- package/dashboard/dist/assets/{DropZone-zxH-9B8c.js → DropZone-C3NdMtwK.js} +2 -2
- package/dashboard/dist/assets/{DropZone-zxH-9B8c.js.map → DropZone-C3NdMtwK.js.map} +1 -1
- package/dashboard/dist/assets/{ElapsedCell-Dr1cEwoi.js → ElapsedCell-DNH-XHOS.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-Dr1cEwoi.js.map → ElapsedCell-DNH-XHOS.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationListSchemaPage-CLdsR-hK.js → EscalationListSchemaPage-BIW1qxjC.js} +3 -3
- package/dashboard/dist/assets/{EscalationListSchemaPage-CLdsR-hK.js.map → EscalationListSchemaPage-BIW1qxjC.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationSchemaPage-DnEnvolM.js → EscalationSchemaPage-Fj2IwQXQ.js} +2 -2
- package/dashboard/dist/assets/{EscalationSchemaPage-DnEnvolM.js.map → EscalationSchemaPage-Fj2IwQXQ.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationsOverview-BH8cxgLI.js → EscalationsOverview-BVUx0vZH.js} +2 -2
- package/dashboard/dist/assets/{EscalationsOverview-BH8cxgLI.js.map → EscalationsOverview-BVUx0vZH.js.map} +1 -1
- package/dashboard/dist/assets/{EventTable-p-xsfYYg.js → EventTable-Dm4zZaRl.js} +2 -2
- package/dashboard/dist/assets/{EventTable-p-xsfYYg.js.map → EventTable-Dm4zZaRl.js.map} +1 -1
- package/dashboard/dist/assets/{GraphInvokePage-Cf9c_LJ-.js → GraphInvokePage-D4f074qh.js} +2 -2
- package/dashboard/dist/assets/GraphInvokePage-D4f074qh.js.map +1 -0
- package/dashboard/dist/assets/HomePage-DQ0bb_M_.js +2 -0
- package/dashboard/dist/assets/HomePage-DQ0bb_M_.js.map +1 -0
- package/dashboard/dist/assets/ListToolbar-CItVkLQ-.js +2 -0
- package/dashboard/dist/assets/{ListToolbar-CDlol8XJ.js.map → ListToolbar-CItVkLQ-.js.map} +1 -1
- package/dashboard/dist/assets/{McpOverview-ChZ36p9t.js → McpOverview-ByYVDldF.js} +2 -2
- package/dashboard/dist/assets/{McpOverview-ChZ36p9t.js.map → McpOverview-ByYVDldF.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryDetailPage-X4j_jm3r.js → McpQueryDetailPage-DYuIdJiH.js} +2 -2
- package/dashboard/dist/assets/{McpQueryDetailPage-X4j_jm3r.js.map → McpQueryDetailPage-DYuIdJiH.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryPage-C30HsQfZ.js → McpQueryPage-C9C4mPC0.js} +2 -2
- package/dashboard/dist/assets/{McpQueryPage-C30HsQfZ.js.map → McpQueryPage-C9C4mPC0.js.map} +1 -1
- package/dashboard/dist/assets/McpRunDetailPage-CWHKhx8n.js +2 -0
- package/dashboard/dist/assets/McpRunDetailPage-CWHKhx8n.js.map +1 -0
- package/dashboard/dist/assets/{McpRunsPage-BHZHCwMM.js → McpRunsPage-DMamEjjr.js} +2 -2
- package/dashboard/dist/assets/{McpRunsPage-BHZHCwMM.js.map → McpRunsPage-DMamEjjr.js.map} +1 -1
- package/dashboard/dist/assets/{NamespacePill-BQNMfY0m.js → NamespacePill-B3YOa1xo.js} +2 -2
- package/dashboard/dist/assets/{NamespacePill-BQNMfY0m.js.map → NamespacePill-B3YOa1xo.js.map} +1 -1
- package/dashboard/dist/assets/OperationsPage-CVPDugQz.js +2 -0
- package/dashboard/dist/assets/OperationsPage-CVPDugQz.js.map +1 -0
- package/dashboard/dist/assets/{OperatorDashboard-BGJrxLMU.js → OperatorDashboard-CK-4GREp.js} +2 -2
- package/dashboard/dist/assets/{OperatorDashboard-BGJrxLMU.js.map → OperatorDashboard-CK-4GREp.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeader-BIY1YvKR.js → PageHeader-poyTWcUA.js} +2 -2
- package/dashboard/dist/assets/{PageHeader-BIY1YvKR.js.map → PageHeader-poyTWcUA.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeaderWithStats-TUyglVW0.js → PageHeaderWithStats-CAmspbl9.js} +2 -2
- package/dashboard/dist/assets/{PageHeaderWithStats-TUyglVW0.js.map → PageHeaderWithStats-CAmspbl9.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessDetailPage-Bcp3GKFo.js → ProcessDetailPage-BBCwv1bQ.js} +2 -2
- package/dashboard/dist/assets/{ProcessDetailPage-Bcp3GKFo.js.map → ProcessDetailPage-BBCwv1bQ.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessesListPage-DPrvuLoU.js → ProcessesListPage-m70xmTZx.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-DPrvuLoU.js.map → ProcessesListPage-m70xmTZx.js.map} +1 -1
- package/dashboard/dist/assets/RoleDetailPage-l865Cx3n.js +8 -0
- package/dashboard/dist/assets/RoleDetailPage-l865Cx3n.js.map +1 -0
- package/dashboard/dist/assets/RolePill-C0SWz9X7.js +2 -0
- package/dashboard/dist/assets/RolePill-C0SWz9X7.js.map +1 -0
- package/dashboard/dist/assets/RolesPage-Wzfjz7Wc.js +2 -0
- package/dashboard/dist/assets/RolesPage-Wzfjz7Wc.js.map +1 -0
- package/dashboard/dist/assets/RunAsSelector-DlnIolTf.js +2 -0
- package/dashboard/dist/assets/{RunAsSelector-Z2Yunqdf.js.map → RunAsSelector-DlnIolTf.js.map} +1 -1
- package/dashboard/dist/assets/{SlidePanel-DK4sWXmp.js → SlidePanel-Bm9B6zIA.js} +2 -2
- package/dashboard/dist/assets/{SlidePanel-DK4sWXmp.js.map → SlidePanel-Bm9B6zIA.js.map} +1 -1
- package/dashboard/dist/assets/{StickyPagination-CeLsnCIS.js → StickyPagination-BHF5fTt5.js} +2 -2
- package/dashboard/dist/assets/{StickyPagination-CeLsnCIS.js.map → StickyPagination-BHF5fTt5.js.map} +1 -1
- package/dashboard/dist/assets/{StreamMessageDetail-DWs7XkmH.js → StreamMessageDetail-Dgp1lzay.js} +2 -2
- package/dashboard/dist/assets/{StreamMessageDetail-DWs7XkmH.js.map → StreamMessageDetail-Dgp1lzay.js.map} +1 -1
- package/dashboard/dist/assets/{SwimlaneTimeline-Dxl9a51q.js → SwimlaneTimeline-BlptZ8lr.js} +2 -2
- package/dashboard/dist/assets/{SwimlaneTimeline-Dxl9a51q.js.map → SwimlaneTimeline-BlptZ8lr.js.map} +1 -1
- package/dashboard/dist/assets/{TagInput-CK-JhBG2.js → TagInput-CM3AV5Ky.js} +2 -2
- package/dashboard/dist/assets/{TagInput-CK-JhBG2.js.map → TagInput-CM3AV5Ky.js.map} +1 -1
- package/dashboard/dist/assets/{TaskDetailPage-DVr31zDE.js → TaskDetailPage-f5K9dVPG.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-DVr31zDE.js.map → TaskDetailPage-f5K9dVPG.js.map} +1 -1
- package/dashboard/dist/assets/{TaskQueuePill-C_Vo2Clc.js → TaskQueuePill-BZYmqdcv.js} +2 -2
- package/dashboard/dist/assets/{TaskQueuePill-C_Vo2Clc.js.map → TaskQueuePill-BZYmqdcv.js.map} +1 -1
- package/dashboard/dist/assets/{TasksListPage-DNGp5lfi.js → TasksListPage-BPyPG6UY.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-DNGp5lfi.js.map → TasksListPage-BPyPG6UY.js.map} +1 -1
- package/dashboard/dist/assets/{TimeAgo-j2Q4w4lY.js → TimeAgo-CBaGbueK.js} +2 -2
- package/dashboard/dist/assets/{TimeAgo-j2Q4w4lY.js.map → TimeAgo-CBaGbueK.js.map} +1 -1
- package/dashboard/dist/assets/{TimestampCell-BLEfob2C.js → TimestampCell-Z5kDrR9U.js} +2 -2
- package/dashboard/dist/assets/{TimestampCell-BLEfob2C.js.map → TimestampCell-Z5kDrR9U.js.map} +1 -1
- package/dashboard/dist/assets/ToolPill-D_-m3lUO.js +2 -0
- package/dashboard/dist/assets/{ToolPill-BCi101Jh.js.map → ToolPill-D_-m3lUO.js.map} +1 -1
- package/dashboard/dist/assets/{ToolTestPanel-ZJZT_mDP.js → ToolTestPanel-DFrP_aMx.js} +2 -2
- package/dashboard/dist/assets/{ToolTestPanel-ZJZT_mDP.js.map → ToolTestPanel-DFrP_aMx.js.map} +1 -1
- package/dashboard/dist/assets/TopicDetailPage-Cj9qXnTW.js +9 -0
- package/dashboard/dist/assets/TopicDetailPage-Cj9qXnTW.js.map +1 -0
- package/dashboard/dist/assets/{TopicsPage-Ctb_85Tw.js → TopicsPage-Dyq6niOv.js} +2 -2
- package/dashboard/dist/assets/{TopicsPage-Ctb_85Tw.js.map → TopicsPage-Dyq6niOv.js.map} +1 -1
- package/dashboard/dist/assets/{UserName-CkFpUqVK.js → UserName-DOX2P-8M.js} +2 -2
- package/dashboard/dist/assets/{UserName-CkFpUqVK.js.map → UserName-DOX2P-8M.js.map} +1 -1
- package/dashboard/dist/assets/WorkflowExecutionPage-Cb_dSIoq.js +2 -0
- package/dashboard/dist/assets/WorkflowExecutionPage-Cb_dSIoq.js.map +1 -0
- package/dashboard/dist/assets/WorkflowPill-CyYYPB7b.js +2 -0
- package/dashboard/dist/assets/{WorkflowPill-DHxrAkp3.js.map → WorkflowPill-CyYYPB7b.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsDashboard-DOyZbqzf.js → WorkflowsDashboard-DJpMO5IY.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsDashboard-DOyZbqzf.js.map → WorkflowsDashboard-DJpMO5IY.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsOverview-CRyHvUBx.js → WorkflowsOverview-CCC_KaXz.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsOverview-CRyHvUBx.js.map → WorkflowsOverview-CCC_KaXz.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-DRBmQXY6.js → YamlWorkflowDetailPage-C3ntUyoZ.js} +11 -11
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-DRBmQXY6.js.map → YamlWorkflowDetailPage-C3ntUyoZ.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowsPage-HnI-9eBW.js → YamlWorkflowsPage-ju-9JDXg.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowsPage-HnI-9eBW.js.map → YamlWorkflowsPage-ju-9JDXg.js.map} +1 -1
- package/dashboard/dist/assets/{agents-BA2CvQO9.js → agents-gs9fea8r.js} +2 -2
- package/dashboard/dist/assets/{agents-BA2CvQO9.js.map → agents-gs9fea8r.js.map} +1 -1
- package/dashboard/dist/assets/bots-DYxAHrob.js +2 -0
- package/dashboard/dist/assets/bots-DYxAHrob.js.map +1 -0
- package/dashboard/dist/assets/{capabilities-BYBKLRRI.js → capabilities-BJoxz6DJ.js} +2 -2
- package/dashboard/dist/assets/{capabilities-BYBKLRRI.js.map → capabilities-BJoxz6DJ.js.map} +1 -1
- package/dashboard/dist/assets/{controlplane-H0Ljhb-1.js → controlplane-BeDZgDDf.js} +2 -2
- package/dashboard/dist/assets/{controlplane-H0Ljhb-1.js.map → controlplane-BeDZgDDf.js.map} +1 -1
- package/dashboard/dist/assets/escalation-columns-CZ4fl0IE.js +2 -0
- package/dashboard/dist/assets/escalation-columns-CZ4fl0IE.js.map +1 -0
- package/dashboard/dist/assets/index-BMM3Xug7.js +2 -0
- package/dashboard/dist/assets/index-BMM3Xug7.js.map +1 -0
- package/dashboard/dist/assets/{index-BXrm76Hm.js → index-BMXkxgnK.js} +2 -2
- package/dashboard/dist/assets/{index-BXrm76Hm.js.map → index-BMXkxgnK.js.map} +1 -1
- package/dashboard/dist/assets/{index-CWkAN6wc.js → index-Bj1Mjjiv.js} +2 -2
- package/dashboard/dist/assets/{index-CWkAN6wc.js.map → index-Bj1Mjjiv.js.map} +1 -1
- package/dashboard/dist/assets/index-BqVb13tE.js +5 -0
- package/dashboard/dist/assets/index-BqVb13tE.js.map +1 -0
- package/dashboard/dist/assets/{index-YWvrMcqh.js → index-CGrtjUdj.js} +2 -2
- package/dashboard/dist/assets/{index-YWvrMcqh.js.map → index-CGrtjUdj.js.map} +1 -1
- package/dashboard/dist/assets/{index-DSjih1iX.js → index-CQ8cd4JZ.js} +2 -2
- package/dashboard/dist/assets/{index-DSjih1iX.js.map → index-CQ8cd4JZ.js.map} +1 -1
- package/dashboard/dist/assets/{index-D1Q8WfGN.js → index-CruL35yU.js} +2 -2
- package/dashboard/dist/assets/{index-D1Q8WfGN.js.map → index-CruL35yU.js.map} +1 -1
- package/dashboard/dist/assets/index-DAM1Dm1F.js +2 -0
- package/dashboard/dist/assets/index-DAM1Dm1F.js.map +1 -0
- package/dashboard/dist/assets/{index-CP6D_Gtw.js → index-DZ7PWRg1.js} +2 -2
- package/dashboard/dist/assets/{index-CP6D_Gtw.js.map → index-DZ7PWRg1.js.map} +1 -1
- package/dashboard/dist/assets/{index-BI-9M7z3.js → index-DfbrPOP3.js} +2 -2
- package/dashboard/dist/assets/{index-BI-9M7z3.js.map → index-DfbrPOP3.js.map} +1 -1
- package/dashboard/dist/assets/index-DwuM7Fka.js +5 -0
- package/dashboard/dist/assets/index-DwuM7Fka.js.map +1 -0
- package/dashboard/dist/assets/index-DycfrrTK.js +63 -0
- package/dashboard/dist/assets/index-DycfrrTK.js.map +1 -0
- package/dashboard/dist/assets/{index-Dx6-4esw.js → index-tbvwCdvQ.js} +2 -2
- package/dashboard/dist/assets/{index-Dx6-4esw.js.map → index-tbvwCdvQ.js.map} +1 -1
- package/dashboard/dist/assets/index-zP2ncT4m.css +1 -0
- package/dashboard/dist/assets/{knowledge-BVbElRHf.js → knowledge-DZnI1Fdk.js} +2 -2
- package/dashboard/dist/assets/{knowledge-BVbElRHf.js.map → knowledge-DZnI1Fdk.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-CstDdowJ.js → mcp-BB_m6UyI.js} +2 -2
- package/dashboard/dist/assets/{mcp-CstDdowJ.js.map → mcp-BB_m6UyI.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-DHUs0N8H.js → mcp-query-BTSWn0de.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-DHUs0N8H.js.map → mcp-query-BTSWn0de.js.map} +1 -1
- package/dashboard/dist/assets/{pipelines-BDmAzE3U.js → pipelines-CvGQLyUz.js} +2 -2
- package/dashboard/dist/assets/{pipelines-BDmAzE3U.js.map → pipelines-CvGQLyUz.js.map} +1 -1
- package/dashboard/dist/assets/task-queues-7-sHKPsN.js +2 -0
- package/dashboard/dist/assets/task-queues-7-sHKPsN.js.map +1 -0
- package/dashboard/dist/assets/{tasks--qZocuG-.js → tasks-BbgOhCVm.js} +2 -2
- package/dashboard/dist/assets/{tasks--qZocuG-.js.map → tasks-BbgOhCVm.js.map} +1 -1
- package/dashboard/dist/assets/{topics-P3jkjMsx.js → topics-D9usuYHv.js} +2 -2
- package/dashboard/dist/assets/{topics-P3jkjMsx.js.map → topics-D9usuYHv.js.map} +1 -1
- package/dashboard/dist/assets/{useEventHooks-CqH8Pasj.js → useEventHooks-DxnqYVPg.js} +2 -2
- package/dashboard/dist/assets/{useEventHooks-CqH8Pasj.js.map → useEventHooks-DxnqYVPg.js.map} +1 -1
- package/dashboard/dist/assets/{useNamespace-aI3tPfo3.js → useNamespace-CBAn0o5z.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-aI3tPfo3.js.map → useNamespace-CBAn0o5z.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-CPVyuW8i.js → useYamlActivityEvents-blOlKGDO.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-CPVyuW8i.js.map → useYamlActivityEvents-blOlKGDO.js.map} +1 -1
- package/dashboard/dist/assets/{users-BrcmIH7O.js → users-CYVJHgNM.js} +2 -2
- package/dashboard/dist/assets/{users-BrcmIH7O.js.map → users-CYVJHgNM.js.map} +1 -1
- package/dashboard/dist/assets/{vendor-icons-Qk9qLjF_.js → vendor-icons-B98-XxNc.js} +147 -132
- package/dashboard/dist/assets/vendor-icons-B98-XxNc.js.map +1 -0
- package/dashboard/dist/assets/{workflows-DPhX5L5C.js → workflows-csQAJF0G.js} +2 -2
- package/dashboard/dist/assets/{workflows-DPhX5L5C.js.map → workflows-csQAJF0G.js.map} +1 -1
- package/dashboard/dist/assets/{yaml-workflows-KZI2cufF.js → yaml-workflows-CctsTDby.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-KZI2cufF.js.map → yaml-workflows-CctsTDby.js.map} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/docs/api/http/escalations.md +16 -1
- package/docs/api/http/roles.md +4 -0
- package/docs/api/http/users.md +39 -0
- package/docs/api/sdk/roles.md +5 -0
- package/docs/api/sdk/users.md +39 -0
- package/docs/hitl/pinned-views.md +45 -0
- package/docs/hitl/x-lt-list-schema.md +37 -2
- package/docs/hitl/x-lt-show-if.md +11 -0
- package/docs/hitl/x-lt-widget.md +22 -0
- package/docs/hitl-guide.md +9 -5
- package/docs/schema-enforcement.md +149 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/AgentConfigPage-CW2udbSO.js +0 -16
- package/dashboard/dist/assets/AgentConfigPage-CW2udbSO.js.map +0 -1
- package/dashboard/dist/assets/AvailableEscalationsPage-Dx51E6s_.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-Dx51E6s_.js.map +0 -1
- package/dashboard/dist/assets/BotPicker-KebTxTDi.js +0 -2
- package/dashboard/dist/assets/BotPicker-KebTxTDi.js.map +0 -1
- package/dashboard/dist/assets/CountdownTimer-DFqEYmiF.js +0 -2
- package/dashboard/dist/assets/CountdownTimer-DFqEYmiF.js.map +0 -1
- package/dashboard/dist/assets/CustomDurationPicker-BxyjEX27.js.map +0 -1
- package/dashboard/dist/assets/GraphInvokePage-Cf9c_LJ-.js.map +0 -1
- package/dashboard/dist/assets/HomePage-BdCtKXAV.js +0 -2
- package/dashboard/dist/assets/HomePage-BdCtKXAV.js.map +0 -1
- package/dashboard/dist/assets/ListToolbar-CDlol8XJ.js +0 -2
- package/dashboard/dist/assets/McpRunDetailPage-18d7Iqfy.js +0 -2
- package/dashboard/dist/assets/McpRunDetailPage-18d7Iqfy.js.map +0 -1
- package/dashboard/dist/assets/OperationsPage-GMojwL3q.js +0 -2
- package/dashboard/dist/assets/OperationsPage-GMojwL3q.js.map +0 -1
- package/dashboard/dist/assets/RoleDetailPage-Ws3umQKQ.js +0 -8
- package/dashboard/dist/assets/RoleDetailPage-Ws3umQKQ.js.map +0 -1
- package/dashboard/dist/assets/RolePill-4LO_CuO3.js +0 -2
- package/dashboard/dist/assets/RolePill-4LO_CuO3.js.map +0 -1
- package/dashboard/dist/assets/RolesPage-D3NNU92h.js +0 -2
- package/dashboard/dist/assets/RolesPage-D3NNU92h.js.map +0 -1
- package/dashboard/dist/assets/RunAsSelector-Z2Yunqdf.js +0 -2
- package/dashboard/dist/assets/ToolPill-BCi101Jh.js +0 -2
- package/dashboard/dist/assets/TopicDetailPage-ynUkO1A8.js +0 -9
- package/dashboard/dist/assets/TopicDetailPage-ynUkO1A8.js.map +0 -1
- package/dashboard/dist/assets/WorkflowExecutionPage-CSw2aQJC.js +0 -2
- package/dashboard/dist/assets/WorkflowExecutionPage-CSw2aQJC.js.map +0 -1
- package/dashboard/dist/assets/WorkflowPill-DHxrAkp3.js +0 -2
- package/dashboard/dist/assets/bots-DHHnoo8x.js +0 -2
- package/dashboard/dist/assets/bots-DHHnoo8x.js.map +0 -1
- package/dashboard/dist/assets/escalation-columns-BV3Ztn7D.js +0 -2
- package/dashboard/dist/assets/escalation-columns-BV3Ztn7D.js.map +0 -1
- package/dashboard/dist/assets/index-B4FaugP1.js +0 -2
- package/dashboard/dist/assets/index-B4FaugP1.js.map +0 -1
- package/dashboard/dist/assets/index-BeFjLCIQ.js +0 -5
- package/dashboard/dist/assets/index-BeFjLCIQ.js.map +0 -1
- package/dashboard/dist/assets/index-BrEcQQ1p.css +0 -1
- package/dashboard/dist/assets/index-CVMA5PVQ.js +0 -2
- package/dashboard/dist/assets/index-CVMA5PVQ.js.map +0 -1
- package/dashboard/dist/assets/index-O2wWcCLo.js +0 -5
- package/dashboard/dist/assets/index-O2wWcCLo.js.map +0 -1
- package/dashboard/dist/assets/index-iX8HLLB9.js +0 -63
- package/dashboard/dist/assets/index-iX8HLLB9.js.map +0 -1
- package/dashboard/dist/assets/vendor-icons-Qk9qLjF_.js.map +0 -1
|
@@ -58,6 +58,7 @@ GET /api/escalations
|
|
|
58
58
|
| `exists` | array of strings | Keys that must be present — `metadata ? key` |
|
|
59
59
|
| `roles` | array of strings | Restrict to these roles (narrows within scope, never widens) |
|
|
60
60
|
| `available` | `true`/`false` | `true` = unclaimed/expired only; `false` = held now |
|
|
61
|
+
| `jeopardy` | `1`/`true` | Only rows past their role's priority threshold — the exact predicate behind the Pace Board's priority count, so a jeopardy list's total equals the badge. Age from the role's `priority_facet` metadata timestamp (`created_at` when unset) against `priority_threshold_minutes` (`sla_minutes` when unset); undialed roles contribute no rows |
|
|
61
62
|
| `orderBy` | array of `{field, direction?, numeric?}` | Sort by column or `metadata.<key>` |
|
|
62
63
|
|
|
63
64
|
When any faceted parameter is present the request runs through the scoped faceted query.
|
|
@@ -204,7 +205,21 @@ Resolving an escalation starts a new workflow execution with the resolver's payl
|
|
|
204
205
|
| `resolverPayload` | `object` | yes | The reviewer's decision — injected into `envelope.resolver` |
|
|
205
206
|
| `metadata` | `object` | no | Outcome facets merged into the escalation's GIN-indexed metadata. Records *what happened* (disposition, timing) next to *what was asked*; `@>`-queryable. Distinct from `resolverPayload`, which resumes the workflow and is not indexed |
|
|
206
207
|
|
|
207
|
-
The `resolverPayload` is stored exactly as submitted — the resolver payload is the payload
|
|
208
|
+
The `resolverPayload` is stored exactly as submitted — the resolver payload is the payload, with no server-side mapping of its shape. The client forms the final payload: the React dashboard maps the flat form to the nested payload via each field's `x-lt-bind` before submitting.
|
|
209
|
+
|
|
210
|
+
**Schema enforcement (roles with `enforce_schema`):** every resolve surface validates the submitted `resolverPayload` against the escalation's resolved form schema (`metadata.form_schema` override, else the pinned `lt_role_schemas` snapshot, else the role's latest) and rejects violations with **422** before any state changes. The body is the canonical validation shape, identical across HTTP, SDK, MCP, and CLI — see [Schema Enforcement](../../schema-enforcement.md):
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"error": "resolverPayload failed schema validation (1 violation)",
|
|
215
|
+
"code": "schema_validation",
|
|
216
|
+
"violations": [{ "field": "contact_email", "message": "Enter a valid email address" }],
|
|
217
|
+
"role": "intake-reviewer",
|
|
218
|
+
"schemaVersion": 3
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Bulk surfaces (`resolve-by-ids`, `resolve-all-or-none`) tag each violation with its `escalationId`, and one failing item blocks the batch before anything resolves.
|
|
208
223
|
|
|
209
224
|
**Example request:**
|
|
210
225
|
|
package/docs/api/http/roles.md
CHANGED
|
@@ -268,6 +268,7 @@ Returns all roles with metadata and usage counts.
|
|
|
268
268
|
| `metadata_schema` | `object \| null` | JSON Schema declaring the expected shape of `lt_escalations.metadata` for this role. Drives faceted-query key autocomplete and creation-time validation |
|
|
269
269
|
| `properties` | `object` | Free user-owned bag — icons, colors, tags, etc. No reserved keys |
|
|
270
270
|
| `ops_visible` | `boolean` | When `true`, the role appears as a station on the `/operations` view |
|
|
271
|
+
| `enforce_schema` | `boolean` | When `true`, every resolve surface validates submitted payloads against this role's form schema and rejects violations with `422` (`code: schema_validation`) — see [Schema Enforcement](../../schema-enforcement.md) |
|
|
271
272
|
| `parent_role` | `string \| null` | Parent role in the process dependency graph; `null` for root stations |
|
|
272
273
|
| `sla_minutes` | `number \| null` | Target resolution time in minutes (capacity setting) |
|
|
273
274
|
| `target_per_hour` | `number \| null` | Intended throughput — items resolved per hour (capacity setting) |
|
|
@@ -366,6 +367,7 @@ When the update changes `form_schema` or `metadata_schema`, the new pair is snap
|
|
|
366
367
|
| `metadata_schema` | `object \| null` | JSON Schema for `lt_escalations.metadata` |
|
|
367
368
|
| `properties` | `object \| null` | Free user-owned bag |
|
|
368
369
|
| `ops_visible` | `boolean` | Include in the `/operations` view |
|
|
370
|
+
| `enforce_schema` | `boolean` | Turn server-side resolver schema validation on/off |
|
|
369
371
|
| `parent_role` | `string \| null` | Parent in the process dependency graph |
|
|
370
372
|
| `sla_minutes` | `number \| null` | SLA target in minutes |
|
|
371
373
|
| `target_per_hour` | `number \| null` | Throughput target (items per hour) |
|
|
@@ -373,6 +375,8 @@ When the update changes `form_schema` or `metadata_schema`, the new pair is snap
|
|
|
373
375
|
| `priority_threshold_minutes` | `number \| null` | Priority age threshold in minutes; falls back to `sla_minutes` |
|
|
374
376
|
| `priority_facet` | `string \| null` | Metadata key for the priority age origin (letters, numbers, underscores); falls back to `created_at` |
|
|
375
377
|
| `upstream_roles` | `string[] \| null` | Replace the set of roles this station draws input from across other Operations sequences (omitted = preserve; `null` or `[]` = clear). Every entry must name an existing role other than this one. Distinct from `parent_role`, which places the role in its own sequence. |
|
|
378
|
+
| `list_schema` | `object \| null` | Rich formatting (x-lt-* markup) for this role's escalation LIST page. Versions independently of `form_schema`; the list always renders the latest |
|
|
379
|
+
| `default_pins` | `array \| null` | Pinned-view seeds for members: `[{ label, url, badge? }]` with dashboard-relative `url`s. Members promote, hide, or reorder them via their own preferences (`/api/me/preferences`). `null` clears |
|
|
376
380
|
| `change_summary` | `string` | Label recorded on the schema version snapshot when this update changes a schema field |
|
|
377
381
|
|
|
378
382
|
**Example request** — configure a role as a station in the ops view:
|
package/docs/api/http/users.md
CHANGED
|
@@ -187,3 +187,42 @@ Deletes the user and all associated role assignments (cascade).
|
|
|
187
187
|
```json
|
|
188
188
|
{ "error": "User not found" }
|
|
189
189
|
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Self-Service — `/api/me`
|
|
194
|
+
|
|
195
|
+
Operations on the **authenticated caller** — no ids, no admin gates. Any signed-in user (or service account) may call them.
|
|
196
|
+
|
|
197
|
+
### GET /api/me/preferences
|
|
198
|
+
|
|
199
|
+
The caller's preferences document — a generic per-user JSON store for presentation state (pinned views are the first tenant). Reads `{}` when unset.
|
|
200
|
+
|
|
201
|
+
**Response 200:**
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"preferences": {
|
|
206
|
+
"pinnedViews": [
|
|
207
|
+
{ "id": "pin-x1", "label": "Needs harvesting", "url": "/escalations/available?role=fleet-servicer&jeopardy=1&view=table", "badge": true }
|
|
208
|
+
],
|
|
209
|
+
"hiddenRolePins": ["My machines"]
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### PATCH /api/me/preferences
|
|
215
|
+
|
|
216
|
+
Shallow-merge the body into the caller's preferences: top-level keys overwrite whole, a `null` value deletes its key. The merge is a single guarded statement (no read-then-write) and the stored document is size-capped (~32 KB). Preferences carry presentation state only — URLs and UI choices, never data and never authorization.
|
|
217
|
+
|
|
218
|
+
**Request body** — any JSON object of preference keys:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{ "pinnedViews": [ { "id": "pin-x1", "label": "Needs harvesting", "url": "/escalations/available?role=fleet-servicer&jeopardy=1", "badge": true } ], "theme": null }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Response 200:** the merged document, `{ "preferences": { ... } }`.
|
|
225
|
+
|
|
226
|
+
**Response 400:** body is not a JSON object. **Response 413:** the patch or the merged document would exceed the size cap (nothing is written).
|
|
227
|
+
|
|
228
|
+
See the [Pinned Views guide](../../hitl/pinned-views.md) for how the dashboard uses this store.
|
package/docs/api/sdk/roles.md
CHANGED
|
@@ -32,6 +32,9 @@ interface RoleDetail {
|
|
|
32
32
|
worker_count: number | null;
|
|
33
33
|
priority_threshold_minutes: number | null;
|
|
34
34
|
priority_facet: string | null;
|
|
35
|
+
list_schema: Record<string, any> | null;
|
|
36
|
+
default_pins: { label: string; url: string; badge?: boolean }[] | null;
|
|
37
|
+
upstream_roles: string[];
|
|
35
38
|
user_count: number;
|
|
36
39
|
chain_count: number;
|
|
37
40
|
workflow_count: number;
|
|
@@ -276,6 +279,8 @@ const result = await lt.roles.update({
|
|
|
276
279
|
| `priority_threshold_minutes` | `number \| null` | No | Priority age threshold in minutes; falls back to `sla_minutes` |
|
|
277
280
|
| `priority_facet` | `string \| null` | No | Metadata key for the priority age origin; falls back to `created_at` |
|
|
278
281
|
| `upstream_roles` | `string[] \| null` | No | Replace the set of roles this station draws input from across other Operations sequences (omitted = preserve; `null` or `[]` = clear). Distinct from `parent_role`, which places the role in its own sequence |
|
|
282
|
+
| `list_schema` | `object \| null` | No | Rich formatting (x-lt-* markup) for this role's escalation LIST page; versions independently of `form_schema` |
|
|
283
|
+
| `default_pins` | `array \| null` | No | Pinned-view seeds for members: `[{ label, url, badge? }]` with dashboard-relative `url`s. Members promote, hide, or reorder via their own preferences (`lt.me`) |
|
|
279
284
|
| `change_summary` | `string` | No | Label recorded on the schema version snapshot when this update changes a schema field |
|
|
280
285
|
|
|
281
286
|
When the update changes `form_schema` or `metadata_schema`, the new pair is snapshotted into the role's version history and `current_schema_version` advances.
|
package/docs/api/sdk/users.md
CHANGED
|
@@ -219,3 +219,42 @@ const result = await lt.users.removeRole({
|
|
|
219
219
|
**Returns:** `LTApiResult<{ removed: true }>` -- returns 404 if role not found.
|
|
220
220
|
|
|
221
221
|
**Auth:** Not required
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## me — the authenticated caller
|
|
226
|
+
|
|
227
|
+
Self-service operations bound to the client's auth context (no ids, no admin gates).
|
|
228
|
+
|
|
229
|
+
### me.getPreferences
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const result = await lt.me.getPreferences();
|
|
233
|
+
// { status: 200, data: { preferences: { pinnedViews: [...] } } }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Returns:** `LTApiResult<{ preferences: Record<string, unknown> }>` — `{}` when unset.
|
|
237
|
+
|
|
238
|
+
### me.patchPreferences
|
|
239
|
+
|
|
240
|
+
Shallow-merge a patch into the caller's preferences: top-level keys overwrite whole, `null` deletes a key. The merge is one guarded statement; the stored document is size-capped (~32 KB → 413).
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const result = await lt.me.patchPreferences({
|
|
244
|
+
patch: {
|
|
245
|
+
pinnedViews: [
|
|
246
|
+
{ id: 'pin-x1', label: 'Needs harvesting', url: '/escalations/available?role=fleet-servicer&jeopardy=1', badge: true },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Parameters:**
|
|
253
|
+
|
|
254
|
+
| Field | Type | Required | Description |
|
|
255
|
+
|-------|------|----------|-------------|
|
|
256
|
+
| `patch` | `object` | Yes | Preference keys to merge; a `null` value deletes its key |
|
|
257
|
+
|
|
258
|
+
**Returns:** `LTApiResult<{ preferences: Record<string, unknown> }>` — the merged document.
|
|
259
|
+
|
|
260
|
+
**Auth:** Required (the operation targets the caller).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Pinned Views
|
|
2
|
+
|
|
3
|
+
Task-queue personas live in a handful of exact queries — "pending harvest rows, oldest first," "everything about serial X." Every filter on the escalations list is already deep-linked in the URL (`role`, `status`, `facets`, `range`, `orderBy`, `view`, `jeopardy`); pinned views add persistence and placement: the right queries sit in the right person's nav from first login.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## User Preferences
|
|
8
|
+
|
|
9
|
+
Pins live in a per-user preferences store — a generic JSON document on the user record (pinned views are its first tenant, not its schema):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
GET /api/me/preferences → { preferences }
|
|
13
|
+
PATCH /api/me/preferences shallow top-level merge; null deletes a key
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The merge is a single guarded statement (no read-then-write) and the document is size-capped (~32 KB → 413). Preferences carry presentation state only — URLs and UI choices, never data and never authorization. A pin to a query the user cannot read simply renders that query's normal empty state; read-scope enforcement stays where it already lives.
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"pinnedViews": [
|
|
21
|
+
{ "id": "pin-x1", "label": "Needs harvesting", "url": "/escalations/available?role=fleet-servicer&facets=%7B%22machineState%22%3A%22finished%22%7D&view=table", "badge": true }
|
|
22
|
+
],
|
|
23
|
+
"hiddenRolePins": ["My machines"]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## The Pinned Section
|
|
28
|
+
|
|
29
|
+
Pins render as a **Pinned** nav section: the user's own pins first, in stored order (drag to reorder, ✕ to remove), then role-provided defaults. **Pin this view** on the escalations list captures the live filter set into a new pin, prompting only for a label.
|
|
30
|
+
|
|
31
|
+
`badge: true` renders a live count beside the label using the same server-side predicate the pin opens onto (the pin's URL is parsed back into its query and counted with `limit: 1`) — so the badge and the list always agree, including `jeopardy=1` pins. Counts refresh on escalation events, and a pin whose URL isn't a countable escalations list renders without a badge.
|
|
32
|
+
|
|
33
|
+
## Role Default Pins
|
|
34
|
+
|
|
35
|
+
A role may declare `default_pins: [{ label, url, badge? }]` (edited on Role Detail → Default Pins). Members see them in their Pinned section from first login, marked as role-provided, and may:
|
|
36
|
+
|
|
37
|
+
- **Promote** — copy into their own pins (an own pin with the same label supersedes the role default)
|
|
38
|
+
- **Hide** — dismiss it (recorded in `hiddenRolePins`)
|
|
39
|
+
- **Reorder** — promote first, then drag among their own pins
|
|
40
|
+
|
|
41
|
+
Membership *is* the persona's bookmark set — no per-user setup. Duplicate labels across a user's roles collapse to the first role's pin.
|
|
42
|
+
|
|
43
|
+
## Reference Example
|
|
44
|
+
|
|
45
|
+
The `fleet-servicer` seed (`examples/seed-fleet-sim.ts`) ships a facet-board list schema, jeopardy dials, and three default pins — the board, a badged "Needs harvesting" facet query, and a badged jeopardy view — so the whole persona story is exercisable from the dashboard. The seeded `reviewer` user is a member.
|
|
@@ -12,11 +12,15 @@ Every string is a markdown/text template run through the same `{{domain.path}}`
|
|
|
12
12
|
|
|
13
13
|
| Key | Level | Purpose |
|
|
14
14
|
|-----|-------|---------|
|
|
15
|
-
| `x-lt-layout` | schema | `"active-history"`, `"active"`, `"facet-table"`, or `"table"` |
|
|
15
|
+
| `x-lt-layout` | schema | `"active-history"`, `"active"`, `"facet-table"`, `"facet-board"`, or `"table"` |
|
|
16
16
|
| `x-lt-help` | schema | Optional markdown header, interpolated with the active row |
|
|
17
17
|
| `x-lt-active` | schema | The live item card: `{ title, subtitle?, body?, fields?: [{ label, value }] }` |
|
|
18
18
|
| `x-lt-history` | schema | History column: `{ row: { title, subtitle?, meta? }, limit?, status? }` |
|
|
19
|
-
| `x-lt-columns` | schema | Column definitions for `facet-table` layout: `[{ label
|
|
19
|
+
| `x-lt-columns` | schema | Column definitions for `facet-table` layout: `[{ label, value, format? }]` |
|
|
20
|
+
| `x-lt-group-by` | schema | `facet-board`: the `"domain.path"` whose value identifies each entity |
|
|
21
|
+
| `x-lt-card` | schema | `facet-board`: the per-entity card — `{ title, state?, fields?: [{ label, value, format? }] }` |
|
|
22
|
+
|
|
23
|
+
`format: "age"` on a `facet-table` column or `facet-board` field renders a timestamp as a compact age (`12m`, `3h`, `2d`) with the absolute time as its tooltip, repainted each minute — aging interim states are scannable at a glance.
|
|
20
24
|
|
|
21
25
|
The **active** item is the first non-terminal escalation. The **history** column is lazy-loaded — a "Load full history" link fetches resolved items on demand (`status` defaults to `"resolved"`, `limit` to 25). Unknown or absent `x-lt-layout` is a safe no-op that falls back to the table.
|
|
22
26
|
|
|
@@ -73,3 +77,34 @@ Use when the queue contains many concurrent rows and the role's context is best
|
|
|
73
77
|
```
|
|
74
78
|
|
|
75
79
|
A status dot precedes the first column automatically. ISO datetime values render as a readable relative date with a full-timestamp tooltip. Missing token values render as an em dash. Clicking any row navigates to the detail page. `x-lt-help` and `x-lt-active` are ignored in this layout.
|
|
80
|
+
|
|
81
|
+
### `"facet-board"` — entity board
|
|
82
|
+
|
|
83
|
+
Use when the rows describe **entities** (machines, stations) rather than a queue: within the current filter scope, rows group by the resolved `x-lt-group-by` value and each group renders one card from its most recent row (by `created_at`). The board reflects the scope — groups with no matching rows simply don't render.
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"x-lt-layout": "facet-board",
|
|
88
|
+
"x-lt-group-by": "metadata.fleetMachine",
|
|
89
|
+
"x-lt-card": {
|
|
90
|
+
"title": "{{metadata.fleetMachine}}",
|
|
91
|
+
"state": "{{metadata.machineState}}",
|
|
92
|
+
"fields": [
|
|
93
|
+
{ "label": "PO", "value": "{{metadata.po}}" },
|
|
94
|
+
{ "label": "Order", "value": "{{metadata.orderId}}" },
|
|
95
|
+
{ "label": "Since", "value": "{{escalation.created_at}}", "format": "age" }
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The card's `state` renders as a status chip (a stable hue per token — commonly a subtype or a metadata state facet). The grid wraps to the viewport (wall-screen friendly). `x-lt-help` renders above the board as in `facet-table`. In the digital-twin pattern — each machine advertising one live pending row — the board is exact by construction; for wider scopes it groups the fetched page, with standard pagination beyond it.
|
|
102
|
+
|
|
103
|
+
Cards are fully interactive (`x-lt-group-by` should be a `metadata.*` path so the facet affordances apply):
|
|
104
|
+
|
|
105
|
+
- **Click** opens the group's latest row in the detail view.
|
|
106
|
+
- **⇧ click** adds the entity's facet to the live filter set — additive, so shift-clicking narrows the board in place and the facet chips row shows each condition with its own remove.
|
|
107
|
+
- **Hovering the card footer** reveals the entity's history as the filtered **table** or **timeline** view (the same view modes the queue offers everywhere else).
|
|
108
|
+
- **Fields bound to a pure `{{metadata.KEY}}` token** carry the same hover pair the table's metadata cells offer: **filter** (narrows within the current role; ⇧ click adds to the current filters) and **search** (the facet across all roles).
|
|
109
|
+
|
|
110
|
+
The reference: `examples/seed-fleet-sim.ts` — one advert per machine, a `format: "age"` "Since" field, and role default pins (see [pinned-views.md](pinned-views.md)).
|
|
@@ -10,6 +10,17 @@ A field can be shown or hidden based on a value in the escalation context. Apply
|
|
|
10
10
|
|
|
11
11
|
The value at `domain.path` is evaluated for truthiness. If truthy, the field shows; if absent, null, false, or an empty string, it is hidden. Prefix `!` to invert — show when the value is absent or falsy.
|
|
12
12
|
|
|
13
|
+
### Equality forms
|
|
14
|
+
|
|
15
|
+
Compare the resolved value's string form against a literal:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"x-lt-showIf": "resolver.designatedStation=DRAFT"
|
|
19
|
+
"x-lt-showIf": "resolver.designatedStation!=DRAFT"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The expected value is the raw remainder after the operator (trimmed; no quoting). Numbers and booleans compare via their string form (`metadata.count=3`, `metadata.live=true`). An absent value compares as the empty string — so `=X` is false and `!=X` is true when the path is missing. `resolver.` conditions react live as the user edits, exactly like the truthy form — this is what drives per-designation sub-surfaces on verdict forms (different fields for send-to-design vs send-to-printing).
|
|
23
|
+
|
|
13
24
|
### Domains
|
|
14
25
|
|
|
15
26
|
| Domain | Resolves against |
|
package/docs/hitl/x-lt-widget.md
CHANGED
|
@@ -10,6 +10,7 @@ For rich inputs beyond standard HTML types, set `x-lt-widget` on any field:
|
|
|
10
10
|
| `"rich-text"` | Tall textarea for formatted text input. |
|
|
11
11
|
| `"markdown"` | Markdown source rendered with headings, tables, lists, code blocks. Editable by default; set `readOnly: true` for a pure content block. |
|
|
12
12
|
| `"checklist"` | Dynamic labeled checkboxes driven by runtime data. Items come from a `"domain.path"` declared in `x-lt-source`. |
|
|
13
|
+
| `"attachment"` | ReadOnly display of a captured binary (data URL or fetchable URL): images inline with click-to-full-size, everything else as a safe open/download link. `"image"` is an accepted alias. |
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -75,6 +76,27 @@ The submitted value is a PNG data URL of the canvas drawing.
|
|
|
75
76
|
|
|
76
77
|
---
|
|
77
78
|
|
|
79
|
+
## Attachment
|
|
80
|
+
|
|
81
|
+
ReadOnly display of a binary captured earlier — typically by the `file-upload` widget on a prior form. The reporting form uploads the evidence (a defect photo, a signed PDF, a scan); the review form shows it:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"properties": {
|
|
86
|
+
"defect_evidence": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"readOnly": true,
|
|
89
|
+
"x-lt-widget": "attachment",
|
|
90
|
+
"description": "Uploaded by the reporting form"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The value is a data URL or a fetchable URL. Rendering dispatches on content type: `image/*` renders inline (constrained, click for full size); everything else — PDFs, scans, arbitrary binaries — renders as a labeled open/download affordance and is never embedded as a document (inlining arbitrary `data:` content is an XSS vector; the image context is script-inert). `"image"` is an accepted alias when the schema wants to name the constrained intent. An empty value reads as a quiet "No attachment".
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
78
100
|
## Markdown
|
|
79
101
|
|
|
80
102
|
Without `readOnly`, the field is a markdown editor — the resolver writes source in a Write/Preview toggle and the submitted value is the markdown text.
|
package/docs/hitl-guide.md
CHANGED
|
@@ -19,7 +19,7 @@ This works because the form is data: a JSON Schema stored on the role, versioned
|
|
|
19
19
|
| Fields that appear based on another answer | `x-lt-showIf` | [x-lt-show-if.md](hitl/x-lt-show-if.md) |
|
|
20
20
|
| Sections, columns, ordering, side-panel help | Layout keywords | [x-lt-layout.md](hitl/x-lt-layout.md) |
|
|
21
21
|
| Runtime-driven items, files, signatures, SOP blocks | Widgets | [x-lt-widget.md](hitl/x-lt-widget.md) |
|
|
22
|
-
| A role-authored list page
|
|
22
|
+
| A role-authored list page — document views, facet tables, entity card boards | List schema | [x-lt-list-schema.md](hitl/x-lt-list-schema.md) |
|
|
23
23
|
| A fully custom UI nothing above can express | Iframe viewport | [iframe.md](hitl/iframe.md) |
|
|
24
24
|
|
|
25
25
|
---
|
|
@@ -62,6 +62,7 @@ When you author a HITL-backed workflow, the platform handles:
|
|
|
62
62
|
- **Escalation chains** — users can re-route work to other roles
|
|
63
63
|
- **AI triage** — optional auto-resolution for common patterns
|
|
64
64
|
- **Credential security** — password fields use ephemeral tokens, never stored in plain text
|
|
65
|
+
- **Schema enforcement** — roles with `enforce_schema` validate every resolver payload server-side (dashboard, API, MCP, CLI alike) with the same pass the form runs, rejecting violations as a structured 422 (see [schema-enforcement.md](schema-enforcement.md))
|
|
65
66
|
- **Telemetry** — trace IDs link escalations to OpenTelemetry traces
|
|
66
67
|
- **Bulk operations** — bulk claim, assign, escalate, triage, and cancel for queue management
|
|
67
68
|
- **Cancellation** — cancel pending escalations from the API or dashboard
|
|
@@ -82,10 +83,11 @@ Ordered as a learning path — each file adds one capability to the same form:
|
|
|
82
83
|
| Conditional visibility (`x-lt-showIf`, `x-lt-hide-if-empty`) | [x-lt-show-if.md](hitl/x-lt-show-if.md) |
|
|
83
84
|
| Layout, ordering, sections, binding, help panel | [x-lt-layout.md](hitl/x-lt-layout.md) |
|
|
84
85
|
| Custom widgets (checklist, file upload, code editor, signature, markdown) | [x-lt-widget.md](hitl/x-lt-widget.md) |
|
|
85
|
-
| List schema (`active-history`, `facet-table`) | [x-lt-list-schema.md](hitl/x-lt-list-schema.md) |
|
|
86
|
+
| List schema (`active-history`, `active`, `facet-table`, `facet-board` cards) | [x-lt-list-schema.md](hitl/x-lt-list-schema.md) |
|
|
86
87
|
| Iframe viewport protocol | [iframe.md](hitl/iframe.md) |
|
|
87
88
|
| Claim lifecycle, resolving from system code, outcome recording, cancellation | [resolution.md](hitl/resolution.md) |
|
|
88
89
|
| Role routing, RBAC, scope, chains | [roles.md](hitl/roles.md) |
|
|
90
|
+
| Pinned views, user preferences, role default pins | [pinned-views.md](hitl/pinned-views.md) |
|
|
89
91
|
|
|
90
92
|
---
|
|
91
93
|
|
|
@@ -93,14 +95,14 @@ Ordered as a learning path — each file adds one capability to the same form:
|
|
|
93
95
|
|
|
94
96
|
| Keyword | Level | Purpose |
|
|
95
97
|
|---------|-------|---------|
|
|
96
|
-
| `x-lt-widget` | field | Rich control: `file-upload`, `code-editor`, `signature`, `rich-text`, `markdown`, `checklist` |
|
|
98
|
+
| `x-lt-widget` | field | Rich control: `file-upload`, `code-editor`, `signature`, `rich-text`, `markdown`, `checklist`, `attachment` (alias `image`) |
|
|
97
99
|
| `x-lt-source` | field | Data path for context-driven widgets: `"domain.path"` |
|
|
98
100
|
| `x-lt-require-all` | field | Checklist completion guard — every item must be checked, except items declared `required: false` |
|
|
99
101
|
| `x-lt-language` | field | Syntax hint for the `code-editor` widget |
|
|
100
102
|
| `accept` | field | File-type filter for `file-upload` (e.g. `".pdf,.png"`) |
|
|
101
103
|
| `x-lt-bind` | field | Path in the resolver payload (e.g. `"customer.email"`) |
|
|
102
104
|
| `x-lt-span` | field | Column span in a `two-column` layout (`2` = full width) |
|
|
103
|
-
| `x-lt-showIf` | field | Show field when a value is truthy at `domain.path`; prefix `!` to invert |
|
|
105
|
+
| `x-lt-showIf` | field | Show field when a value is truthy at `domain.path`; prefix `!` to invert; `=VALUE` / `!=VALUE` compare the string form |
|
|
104
106
|
| `x-lt-hide-if-empty` | field | `true` — suppress the field when its value is null, `""`, `false`, or `0` |
|
|
105
107
|
| `x-lt-section` | field | Section group label |
|
|
106
108
|
| `x-lt-minimum` | field | Dynamic lower bound — resolves a `"domain.path"` from the escalation context |
|
|
@@ -109,7 +111,9 @@ Ordered as a learning path — each file adds one capability to the same form:
|
|
|
109
111
|
| `x-lt-max-length` | field | Dynamic maximum string length — resolves a `"domain.path"` |
|
|
110
112
|
| `x-lt-pattern-error` | field | Human-readable label for a `pattern` validation failure |
|
|
111
113
|
| `x-lt-order` | schema | Field render sequence |
|
|
112
|
-
| `x-lt-layout` | schema | `"two-column"` grid layout (form) or `"active-history"` / `"facet-table"` (list) |
|
|
114
|
+
| `x-lt-layout` | schema | `"two-column"` grid layout (form) or `"active-history"` / `"facet-table"` / `"facet-board"` (list) |
|
|
115
|
+
| `x-lt-group-by` | schema (list) | `facet-board`: the `"domain.path"` identifying each entity |
|
|
116
|
+
| `x-lt-card` | schema (list) | `facet-board`: per-entity card — `{ title, state?, fields? }`; fields accept `format: "age"` |
|
|
113
117
|
| `x-lt-help` | schema | Markdown guidance for the side panel's Help view |
|
|
114
118
|
| `x-lt-context` | schema | Plain-text fallback for the Help view when `x-lt-help` is absent |
|
|
115
119
|
| `x-lt-viewport` | schema | Replace the generated form with a custom iframe UI |
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Resolver Schema Enforcement
|
|
2
|
+
|
|
3
|
+
A role's `form_schema` is an API contract, and the dashboard form is one client
|
|
4
|
+
of it. Escalations resolve from many directions — the dashboard, the SDK, the
|
|
5
|
+
CLI, webhooks that know a signal key, MCP agents, and simulated workforces —
|
|
6
|
+
and every one of them submits the same artifact: a resolver payload shaped by
|
|
7
|
+
the role's form schema and its `x-lt-bind` map. Schema enforcement makes the
|
|
8
|
+
server the authority on that contract: roles that opt in validate every
|
|
9
|
+
submitted payload at the API layer and reject violations before any state
|
|
10
|
+
changes.
|
|
11
|
+
|
|
12
|
+
## The contract
|
|
13
|
+
|
|
14
|
+
A form schema declares, per field:
|
|
15
|
+
|
|
16
|
+
- **presence** — membership in `required`, honoring `x-lt-showIf` (a field the
|
|
17
|
+
submitter cannot see never blocks submission; conditions evaluate against
|
|
18
|
+
the escalation surface plus the submitted values as `resolver.*`)
|
|
19
|
+
- **type** — the declared JSON Schema `type`; a number field takes `1`, and
|
|
20
|
+
`"1"` is a violation
|
|
21
|
+
- **enum** — membership in the field's declared choices
|
|
22
|
+
- **bounds and patterns** — `minimum`/`maximum` (and their `x-lt-*` dynamic
|
|
23
|
+
variants resolved from the escalation context), lengths, `pattern`,
|
|
24
|
+
`format: email` / `format: uri`
|
|
25
|
+
- **checklist completion** — `x-lt-require-all` against the items resolved
|
|
26
|
+
from the row's own envelope
|
|
27
|
+
|
|
28
|
+
The pass that checks these is one isomorphic module
|
|
29
|
+
(`shared/form-validation/`): the dashboard runs it pre-submission on the flat
|
|
30
|
+
form values, and the API layer runs it on the submitted nested payload by
|
|
31
|
+
inverting each field's `x-lt-bind` path first. One implementation on both
|
|
32
|
+
sides means a payload that passes the client panel passes the server gate,
|
|
33
|
+
and a rejection lists exactly the errors the panel would show.
|
|
34
|
+
|
|
35
|
+
## Opting a role in
|
|
36
|
+
|
|
37
|
+
Enforcement is per-role config: set `enforce_schema: true` on the role
|
|
38
|
+
(`PATCH /api/roles/:role`, the `update_role` MCP tool, or the dashboard role
|
|
39
|
+
editor). The schema validated against is resolved per escalation, most
|
|
40
|
+
specific first:
|
|
41
|
+
|
|
42
|
+
1. `metadata.form_schema` — a full form embedded on the row
|
|
43
|
+
2. the `lt_role_schemas` snapshot pinned by `metadata.schema_version`
|
|
44
|
+
3. the role's live (latest) `form_schema`
|
|
45
|
+
|
|
46
|
+
A role that enforces but declares no schema has no contract to enforce, and
|
|
47
|
+
its resolves proceed unchanged.
|
|
48
|
+
|
|
49
|
+
## The rejection shape
|
|
50
|
+
|
|
51
|
+
Every surface reports violations with one canonical body
|
|
52
|
+
(`types/validation.ts`), HTTP status 422:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"error": "resolverPayload failed schema validation (2 violations)",
|
|
57
|
+
"code": "schema_validation",
|
|
58
|
+
"violations": [
|
|
59
|
+
{ "field": "contact_email", "message": "Enter a valid email address" },
|
|
60
|
+
{ "field": "tier", "message": "Must be one of: free, starter, professional, enterprise" }
|
|
61
|
+
],
|
|
62
|
+
"role": "intake-reviewer",
|
|
63
|
+
"schemaVersion": 3
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **HTTP / routes** — the 422 response body
|
|
68
|
+
- **SDK** — `result.status === 422`, `result.code === 'schema_validation'`,
|
|
69
|
+
full body in `result.data`; narrow with `isValidationErrorBody(result.data)`
|
|
70
|
+
- **MCP** (`resolve_escalation`, `claim_and_resolve`) — the tool returns the
|
|
71
|
+
same JSON body with `isError: true`; `check_resolution` reports
|
|
72
|
+
`schema_enforced: true` on pending escalations whose role enforces
|
|
73
|
+
- **CLI** — prints each violation beneath the error line
|
|
74
|
+
- **Dashboard** — maps `violations` into the same errors panel the
|
|
75
|
+
pre-submission pass feeds
|
|
76
|
+
|
|
77
|
+
Bulk surfaces (`resolve-by-ids`, `resolve-all-or-none`) tag each violation
|
|
78
|
+
with its `escalationId`; all-or-none semantics extend to validation — one
|
|
79
|
+
failing item blocks the batch before anything resolves.
|
|
80
|
+
|
|
81
|
+
## Enforcement points
|
|
82
|
+
|
|
83
|
+
All resolve surfaces run the same gate, before any resolution side effect:
|
|
84
|
+
|
|
85
|
+
| Surface | Gate placement |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `POST /escalations/:id/resolve` | after RBAC and claim-liveness checks, before path dispatch |
|
|
88
|
+
| `POST /escalations/resolve-by-signal-key` | after write-scope check |
|
|
89
|
+
| `POST /escalations/resolve-by-ids` | after write-scope check; only rows in enforcing roles load in full |
|
|
90
|
+
| `POST /escalations/resolve-all-or-none` | after the unsupported-path check, before password redaction |
|
|
91
|
+
| `POST /escalations/resolve-by-metadata` | inside the atomic statement: an enforcing target returns `validation_required` with nothing written; the validated payload re-invokes with the row id asserted, and the second pass re-checks `pending` inside the same guarded statement |
|
|
92
|
+
| MCP `resolve_escalation` / `claim_and_resolve` | before the resolve; `claim_and_resolve` validates before claiming so a rejected payload never strands a claim |
|
|
93
|
+
|
|
94
|
+
## Production cost model
|
|
95
|
+
|
|
96
|
+
Role schemas change on admin timescales; resolves happen on work timescales.
|
|
97
|
+
The gate is built so the hot path pays nothing for it:
|
|
98
|
+
|
|
99
|
+
- **The enforcing-role set** (`SELECT role FROM lt_roles WHERE enforce_schema`)
|
|
100
|
+
is cached under `ROLE_ENFORCEMENT_CACHE_TTL_MS` (default 30s). Every surface
|
|
101
|
+
consults the cached set first — with no enforcing roles involved, a resolve
|
|
102
|
+
adds zero SQL.
|
|
103
|
+
- **Pinned schema snapshots** are immutable and cache indefinitely (LRU-bounded).
|
|
104
|
+
- **Latest schemas** cache under the same TTL.
|
|
105
|
+
- In-process role writes invalidate immediately; across a container fleet,
|
|
106
|
+
staleness is bounded by one TTL window. Escalations that pin
|
|
107
|
+
`schema_version` are immune to latest-schema staleness.
|
|
108
|
+
|
|
109
|
+
Surfaces that already load the row (by id, by signal key, all-or-none)
|
|
110
|
+
validate with zero additional reads beyond the cached schema. Bulk-by-ids
|
|
111
|
+
shares its RBAC scope-row read with the gate and loads full rows only for
|
|
112
|
+
enforcing targets.
|
|
113
|
+
|
|
114
|
+
## Typed consumption
|
|
115
|
+
|
|
116
|
+
Enforcement guarantees what entered the system; `parseResolverPayload`
|
|
117
|
+
(exported from the SDK) gives workflow code the consuming half. Declare the
|
|
118
|
+
payload shape once as a zod schema, derive the type from it, and parse
|
|
119
|
+
resolutions through it at the activity boundary:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { z } from 'zod';
|
|
123
|
+
import { parseResolverPayload } from '@longtail/sdk';
|
|
124
|
+
|
|
125
|
+
export const IntakeResolverV1Schema = z.object({
|
|
126
|
+
customer: z.object({ name: z.string(), email: z.string() }),
|
|
127
|
+
contract: z.object({ tier: z.enum(['starter', 'professional']), approved: z.boolean() }),
|
|
128
|
+
});
|
|
129
|
+
export type IntakeResolverV1 = z.infer<typeof IntakeResolverV1Schema>;
|
|
130
|
+
|
|
131
|
+
const intake = parseResolverPayload(IntakeResolverV1Schema, response);
|
|
132
|
+
intake.customer.name; // typed and runtime-checked
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A non-conforming payload throws `ResolverPayloadTypeError` carrying the same
|
|
136
|
+
`{ field, message }` violation list, with the ZodError as `cause`. The
|
|
137
|
+
`rich-form` example (`examples/workflows/rich-form/`) shows the full pattern:
|
|
138
|
+
one zod schema as the single source of truth for the type, the runtime check,
|
|
139
|
+
and the version pin.
|
|
140
|
+
|
|
141
|
+
## Rollout pattern
|
|
142
|
+
|
|
143
|
+
1. Confirm the role's submitters send complete payloads — `check_resolution`
|
|
144
|
+
returns the `form_schema` (fields, types, `x-lt-bind` paths) an agent needs
|
|
145
|
+
to construct one, and flags `schema_enforced` once the role opts in.
|
|
146
|
+
2. Flip `enforce_schema: true` on the role.
|
|
147
|
+
3. Watch for 422s: each one identifies a submitter with a payload gap and
|
|
148
|
+
names the exact fields. The dashboard is unaffected — its pre-submission
|
|
149
|
+
pass is the same code.
|
package/package.json
CHANGED