@rubytech/create-sitedesk-code 0.1.508 → 0.1.510
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/dist/index.js +53 -0
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +555 -0
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +482 -0
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +394 -0
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +232 -0
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +200 -0
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +169 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +12 -7
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/business-assistant/PLUGIN.md +1 -0
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -0
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +68 -28
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +271 -29
- package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +21 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +5 -4
- package/payload/platform/plugins/docs/references/admin-ui.md +10 -5
- package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -0
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +112 -4
- package/payload/platform/scripts/__tests__/check-brand-cache.test.sh +231 -0
- package/payload/platform/scripts/check-brand-cache.sh +138 -0
- package/payload/platform/scripts/lib/account-schema-owned-dirs.py +61 -32
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +17 -30
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +50 -4
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +58 -15
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/server/maxy-edge.js +26 -11
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/AdminLoginScreens-CWMpccrR.css.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-CWMpccrR.css.gz +0 -0
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js +1 -0
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-HeELCCjJ.js → AdminShell-BMPEWNZk.js} +1 -1
- package/payload/server/public/assets/AdminShell-BMPEWNZk.js.br +0 -0
- package/payload/server/public/assets/AdminShell-BMPEWNZk.js.gz +0 -0
- package/payload/server/public/assets/_baseFor-Be3EKK3K.js.br +0 -0
- package/payload/server/public/assets/_baseFor-Be3EKK3K.js.gz +0 -0
- package/payload/server/public/assets/{activity-BEIDAxDA.js → activity-ByWYF5dC.js} +1 -1
- package/payload/server/public/assets/activity-ByWYF5dC.js.br +0 -0
- package/payload/server/public/assets/activity-ByWYF5dC.js.gz +0 -0
- package/payload/server/public/assets/admin-DGr1HuPg.js +1 -0
- package/payload/server/public/assets/admin-DGr1HuPg.js.br +0 -0
- package/payload/server/public/assets/admin-DGr1HuPg.js.gz +0 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js +1 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js.br +0 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js.gz +0 -0
- package/payload/server/public/assets/arc-DaI5MLTq.js.br +0 -0
- package/payload/server/public/assets/arc-DaI5MLTq.js.gz +0 -0
- package/payload/server/public/assets/architecture-YZFGNWBL-DWZIX5aV.js.br +0 -0
- package/payload/server/public/assets/architectureDiagram-Q4EWVU46-CBGVUEN9.js.br +0 -0
- package/payload/server/public/assets/architectureDiagram-Q4EWVU46-CBGVUEN9.js.gz +0 -0
- package/payload/server/public/assets/array-X0JlPOfd.js.br +0 -0
- package/payload/server/public/assets/blockDiagram-DXYQGD6D-DS6dS4GZ.js.br +0 -0
- package/payload/server/public/assets/blockDiagram-DXYQGD6D-DS6dS4GZ.js.gz +0 -0
- package/payload/server/public/assets/{browser-GkVTGqlb.js → browser-4tTaem8N.js} +1 -1
- package/payload/server/public/assets/browser-4tTaem8N.js.br +0 -0
- package/payload/server/public/assets/browser-4tTaem8N.js.gz +0 -0
- package/payload/server/public/assets/c4Diagram-AHTNJAMY-hBSBUZPt.js.br +0 -0
- package/payload/server/public/assets/c4Diagram-AHTNJAMY-hBSBUZPt.js.gz +0 -0
- package/payload/server/public/assets/{calendar-CSqJeFEg.js → calendar-BEjnkKG1.js} +1 -1
- package/payload/server/public/assets/calendar-BEjnkKG1.js.br +0 -0
- package/payload/server/public/assets/calendar-BEjnkKG1.js.gz +0 -0
- package/payload/server/public/assets/channel-BEokjAeE.js.br +0 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js +1 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js.br +0 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DqYv3oFh.js +1 -0
- package/payload/server/public/assets/chevron-right-CQfPGsFb.js +1 -0
- package/payload/server/public/assets/chevron-right-CQfPGsFb.js.br +0 -0
- package/payload/server/public/assets/chunk-2KRD3SAO-B6koUoB7.js.br +0 -0
- package/payload/server/public/assets/chunk-2KRD3SAO-B6koUoB7.js.gz +0 -0
- package/payload/server/public/assets/chunk-336JU56O-BF6BRh8u.js.br +0 -0
- package/payload/server/public/assets/chunk-336JU56O-BF6BRh8u.js.gz +0 -0
- package/payload/server/public/assets/chunk-426QAEUC-pbQ4PWBU.js.br +0 -0
- package/payload/server/public/assets/chunk-426QAEUC-pbQ4PWBU.js.gz +0 -0
- package/payload/server/public/assets/chunk-4BX2VUAB-7sO95xR2.js.br +0 -0
- package/payload/server/public/assets/chunk-4BX2VUAB-7sO95xR2.js.gz +0 -0
- package/payload/server/public/assets/chunk-4TB4RGXK-GCTndJYx.js.br +0 -0
- package/payload/server/public/assets/chunk-4TB4RGXK-GCTndJYx.js.gz +0 -0
- package/payload/server/public/assets/chunk-55IACEB6-ggIJf_TC.js.br +0 -0
- package/payload/server/public/assets/chunk-55IACEB6-ggIJf_TC.js.gz +0 -0
- package/payload/server/public/assets/chunk-5FUZZQ4R-DeGmkimt.js.br +0 -0
- package/payload/server/public/assets/chunk-5FUZZQ4R-DeGmkimt.js.gz +0 -0
- package/payload/server/public/assets/chunk-5PVQY5BW-aFIQKk_U.js.br +0 -0
- package/payload/server/public/assets/chunk-5PVQY5BW-aFIQKk_U.js.gz +0 -0
- package/payload/server/public/assets/chunk-67CJDMHE-LS8rUbii.js.br +2 -0
- package/payload/server/public/assets/chunk-67CJDMHE-LS8rUbii.js.gz +0 -0
- package/payload/server/public/assets/chunk-7N4EOEYR-JKOVngS0.js.br +2 -0
- package/payload/server/public/assets/chunk-7N4EOEYR-JKOVngS0.js.gz +0 -0
- package/payload/server/public/assets/chunk-AA7GKIK3-CoXnTVK6.js.br +0 -0
- package/payload/server/public/assets/chunk-AA7GKIK3-CoXnTVK6.js.gz +0 -0
- package/payload/server/public/assets/chunk-BSJP7CBP-CcsqObFg.js.br +0 -0
- package/payload/server/public/assets/chunk-BSJP7CBP-CcsqObFg.js.gz +0 -0
- package/payload/server/public/assets/chunk-CIAEETIT-CpsybR7q.js.br +0 -0
- package/payload/server/public/assets/chunk-CIAEETIT-CpsybR7q.js.gz +0 -0
- package/payload/server/public/assets/chunk-CltuBf4Z.js.br +0 -0
- package/payload/server/public/assets/chunk-CltuBf4Z.js.gz +0 -0
- package/payload/server/public/assets/chunk-EDXVE4YY-BZ2I9PSq.js.br +0 -0
- package/payload/server/public/assets/chunk-EDXVE4YY-BZ2I9PSq.js.gz +0 -0
- package/payload/server/public/assets/chunk-ENJZ2VHE-DbTwt6OZ.js.br +0 -0
- package/payload/server/public/assets/chunk-ENJZ2VHE-DbTwt6OZ.js.gz +0 -0
- package/payload/server/public/assets/chunk-FMBD7UC4-D27VnSCa.js.br +0 -0
- package/payload/server/public/assets/chunk-FMBD7UC4-D27VnSCa.js.gz +0 -0
- package/payload/server/public/assets/chunk-FOC6F5B3-C0tU2UFl.js.br +0 -0
- package/payload/server/public/assets/chunk-FOC6F5B3-C0tU2UFl.js.gz +0 -0
- package/payload/server/public/assets/chunk-ICPOFSXX-UPPu2OIq.js.br +0 -0
- package/payload/server/public/assets/chunk-ICPOFSXX-UPPu2OIq.js.gz +0 -0
- package/payload/server/public/assets/chunk-K5T4RW27-DIKxqyYw.js.br +0 -0
- package/payload/server/public/assets/chunk-K5T4RW27-DIKxqyYw.js.gz +0 -0
- package/payload/server/public/assets/chunk-KGLVRYIC-CiL4YVLo.js.br +0 -0
- package/payload/server/public/assets/chunk-KGLVRYIC-CiL4YVLo.js.gz +0 -0
- package/payload/server/public/assets/chunk-LIHQZDEY-ejKezVlD.js.br +0 -0
- package/payload/server/public/assets/chunk-LIHQZDEY-ejKezVlD.js.gz +0 -0
- package/payload/server/public/assets/chunk-ORNJ4GCN-Cc0vqc-D.js.br +0 -0
- package/payload/server/public/assets/chunk-ORNJ4GCN-Cc0vqc-D.js.gz +0 -0
- package/payload/server/public/assets/chunk-OYMX7WX6-Drxn1qG0.js.br +0 -0
- package/payload/server/public/assets/chunk-OYMX7WX6-Drxn1qG0.js.gz +0 -0
- package/payload/server/public/assets/chunk-QZHKN3VN-Dhlh7Pkk.js.br +0 -0
- package/payload/server/public/assets/chunk-QZHKN3VN-Dhlh7Pkk.js.gz +0 -0
- package/payload/server/public/assets/chunk-U2HBQHQK-DnbQIkRP.js.br +0 -0
- package/payload/server/public/assets/chunk-U2HBQHQK-DnbQIkRP.js.gz +0 -0
- package/payload/server/public/assets/chunk-X2U36JSP-CauTSVWg.js.br +0 -0
- package/payload/server/public/assets/chunk-X2U36JSP-CauTSVWg.js.gz +0 -0
- package/payload/server/public/assets/chunk-XPW4576I-DZqqB9Zi.js.br +0 -0
- package/payload/server/public/assets/chunk-XPW4576I-DZqqB9Zi.js.gz +0 -0
- package/payload/server/public/assets/chunk-YZCP3GAM-BGHo4Li-.js.br +0 -0
- package/payload/server/public/assets/chunk-YZCP3GAM-BGHo4Li-.js.gz +0 -0
- package/payload/server/public/assets/chunk-ZZ45TVLE-DWGJKPd7.js.br +0 -0
- package/payload/server/public/assets/chunk-ZZ45TVLE-DWGJKPd7.js.gz +0 -0
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-D2z_ZV1x.js.br +0 -0
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-D2z_ZV1x.js.gz +0 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BJwJoNEK.js.br +0 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BJwJoNEK.js.gz +0 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js +1 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js.br +0 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js.gz +0 -0
- package/payload/server/public/assets/clone-Bn7XXVhr.js.br +0 -0
- package/payload/server/public/assets/cose-bilkent-S5V4N54A-D-95BzXL.js.br +0 -0
- package/payload/server/public/assets/cose-bilkent-S5V4N54A-D-95BzXL.js.gz +0 -0
- package/payload/server/public/assets/cytoscape.esm-CI2IWMLB.js.br +0 -0
- package/payload/server/public/assets/cytoscape.esm-CI2IWMLB.js.gz +0 -0
- package/payload/server/public/assets/dagre-BZYNBHvT.js.br +0 -0
- package/payload/server/public/assets/dagre-BZYNBHvT.js.gz +0 -0
- package/payload/server/public/assets/dagre-KV5264BT-DyU4HQLQ.js.br +0 -0
- package/payload/server/public/assets/dagre-KV5264BT-DyU4HQLQ.js.gz +0 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js +1 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js.br +0 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js.gz +0 -0
- package/payload/server/public/assets/defaultLocale-B6RGN4id.js.br +0 -0
- package/payload/server/public/assets/defaultLocale-B6RGN4id.js.gz +0 -0
- package/payload/server/public/assets/diagram-5BDNPKRD-CCttJ9XA.js.br +0 -0
- package/payload/server/public/assets/diagram-5BDNPKRD-CCttJ9XA.js.gz +0 -0
- package/payload/server/public/assets/diagram-G4DWMVQ6-DvkpgSLO.js.br +0 -0
- package/payload/server/public/assets/diagram-G4DWMVQ6-DvkpgSLO.js.gz +0 -0
- package/payload/server/public/assets/diagram-MMDJMWI5-DHmW5Y9k.js.br +0 -0
- package/payload/server/public/assets/diagram-MMDJMWI5-DHmW5Y9k.js.gz +0 -0
- package/payload/server/public/assets/diagram-TYMM5635-C6T4hwzO.js.br +0 -0
- package/payload/server/public/assets/diagram-TYMM5635-C6T4hwzO.js.gz +0 -0
- package/payload/server/public/assets/dist-QvbmK1IP.js.br +0 -0
- package/payload/server/public/assets/dist-QvbmK1IP.js.gz +0 -0
- package/payload/server/public/assets/erDiagram-SMLLAGMA-5v_o7s3T.js.br +0 -0
- package/payload/server/public/assets/erDiagram-SMLLAGMA-5v_o7s3T.js.gz +0 -0
- package/payload/server/public/assets/{file-text-DiscM2pP.js → file-text-DMKw3nIk.js} +1 -1
- package/payload/server/public/assets/file-text-DMKw3nIk.js.br +0 -0
- package/payload/server/public/assets/file-text-DMKw3nIk.js.gz +0 -0
- package/payload/server/public/assets/flatten-BtIc9jX7.js.br +0 -0
- package/payload/server/public/assets/flatten-BtIc9jX7.js.gz +0 -0
- package/payload/server/public/assets/flowDiagram-DWJPFMVM-3UhCDsjt.js.br +0 -0
- package/payload/server/public/assets/flowDiagram-DWJPFMVM-3UhCDsjt.js.gz +0 -0
- package/payload/server/public/assets/ganttDiagram-T4ZO3ILL-F2zgHIIQ.js.br +0 -0
- package/payload/server/public/assets/ganttDiagram-T4ZO3ILL-F2zgHIIQ.js.gz +0 -0
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-C-FvNTHZ.js.br +0 -0
- package/payload/server/public/assets/gitGraphDiagram-UUTBAWPF-oQc1N3dT.js.br +0 -0
- package/payload/server/public/assets/gitGraphDiagram-UUTBAWPF-oQc1N3dT.js.gz +0 -0
- package/payload/server/public/assets/{graph-CRP5DM8C.js → graph-C3vW8lvw.js} +1 -1
- package/payload/server/public/assets/graph-C3vW8lvw.js.br +0 -0
- package/payload/server/public/assets/graph-C3vW8lvw.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-Du-6KoBE.js → graph-labels-BZgd0L6n.js} +1 -1
- package/payload/server/public/assets/graph-labels-BZgd0L6n.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BZgd0L6n.js.gz +0 -0
- package/payload/server/public/assets/graphlib-DY4-AT2Q.js.br +0 -0
- package/payload/server/public/assets/graphlib-DY4-AT2Q.js.gz +0 -0
- package/payload/server/public/assets/info-OMHHGYJF-BjLPgHAC.js.br +0 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-CXNKLTui.js.br +0 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-CXNKLTui.js.gz +0 -0
- package/payload/server/public/assets/init-BmUWJJHz.js.br +0 -0
- package/payload/server/public/assets/init-BmUWJJHz.js.gz +0 -0
- package/payload/server/public/assets/isEmpty-Bu4fZ8Kf.js.br +0 -0
- package/payload/server/public/assets/isEmpty-Bu4fZ8Kf.js.gz +0 -0
- package/payload/server/public/assets/ishikawaDiagram-UXIWVN3A-wJhSOkuw.js.br +0 -0
- package/payload/server/public/assets/ishikawaDiagram-UXIWVN3A-wJhSOkuw.js.gz +0 -0
- package/payload/server/public/assets/journeyDiagram-VCZTEJTY-DgadC6Pz.js.br +0 -0
- package/payload/server/public/assets/journeyDiagram-VCZTEJTY-DgadC6Pz.js.gz +0 -0
- package/payload/server/public/assets/kanban-definition-6JOO6SKY-Bs4nQqXI.js.br +0 -0
- package/payload/server/public/assets/kanban-definition-6JOO6SKY-Bs4nQqXI.js.gz +0 -0
- package/payload/server/public/assets/katex-BjZuzSQn.js.br +0 -0
- package/payload/server/public/assets/katex-BjZuzSQn.js.gz +0 -0
- package/payload/server/public/assets/line-Bnmxy1hO.js.br +0 -0
- package/payload/server/public/assets/line-Bnmxy1hO.js.gz +0 -0
- package/payload/server/public/assets/linear-RvwZxFzh.js.br +0 -0
- package/payload/server/public/assets/linear-RvwZxFzh.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-D-H_uIV-.js → maximize-2-la3IBA3M.js} +1 -1
- package/payload/server/public/assets/maximize-2-la3IBA3M.js.br +0 -0
- package/payload/server/public/assets/maximize-2-la3IBA3M.js.gz +0 -0
- package/payload/server/public/assets/mermaid-parser.core-DeNV8RtA.js.br +0 -0
- package/payload/server/public/assets/mermaid-parser.core-DeNV8RtA.js.gz +0 -0
- package/payload/server/public/assets/mermaid.core-BU9IiSCP.js.br +0 -0
- package/payload/server/public/assets/mermaid.core-BU9IiSCP.js.gz +0 -0
- package/payload/server/public/assets/mindmap-definition-QFDTVHPH-D1PMUxxm.js.br +0 -0
- package/payload/server/public/assets/mindmap-definition-QFDTVHPH-D1PMUxxm.js.gz +0 -0
- package/payload/server/public/assets/{operator-D5rA76YY.js → operator-D9NgVL9_.js} +1 -1
- package/payload/server/public/assets/operator-D9NgVL9_.js.br +0 -0
- package/payload/server/public/assets/operator-D9NgVL9_.js.gz +0 -0
- package/payload/server/public/assets/ordinal-C2TEWKNu.js.br +0 -0
- package/payload/server/public/assets/ordinal-C2TEWKNu.js.gz +0 -0
- package/payload/server/public/assets/packet-4T2RLAQJ-BBAZASfb.js.br +0 -0
- package/payload/server/public/assets/{page-CHK1415C.js → page-BWFHRIAH.js} +1 -1
- package/payload/server/public/assets/page-BWFHRIAH.js.br +0 -0
- package/payload/server/public/assets/page-BWFHRIAH.js.gz +0 -0
- package/payload/server/public/assets/{page-DZE3H_n4.js → page-DC11gesX.js} +1 -1
- package/payload/server/public/assets/page-DC11gesX.js.br +0 -0
- package/payload/server/public/assets/page-DC11gesX.js.gz +0 -0
- package/payload/server/public/assets/path-CoPyR7c2.js.br +0 -0
- package/payload/server/public/assets/path-CoPyR7c2.js.gz +0 -0
- package/payload/server/public/assets/pdf-render-C257cidb.js.br +0 -0
- package/payload/server/public/assets/pdf-render-C257cidb.js.gz +0 -0
- package/payload/server/public/assets/pdf.worker.min-yatZIOMy.mjs.br +0 -0
- package/payload/server/public/assets/pdf.worker.min-yatZIOMy.mjs.gz +0 -0
- package/payload/server/public/assets/pie-ZZUOXDRM-CCJUnMeG.js.br +0 -0
- package/payload/server/public/assets/pieDiagram-DEJITSTG-BzdNzoKA.js.br +0 -0
- package/payload/server/public/assets/pieDiagram-DEJITSTG-BzdNzoKA.js.gz +0 -0
- package/payload/server/public/assets/preload-helper-BQrMkyGX.js.br +0 -0
- package/payload/server/public/assets/preload-helper-BQrMkyGX.js.gz +0 -0
- package/payload/server/public/assets/{public-DFHXVkdD.js → public-DQbbLHQN.js} +1 -1
- package/payload/server/public/assets/public-DQbbLHQN.js.br +0 -0
- package/payload/server/public/assets/public-DQbbLHQN.js.gz +0 -0
- package/payload/server/public/assets/quadrantDiagram-34T5L4WZ-oFr1MJRO.js.br +0 -0
- package/payload/server/public/assets/quadrantDiagram-34T5L4WZ-oFr1MJRO.js.gz +0 -0
- package/payload/server/public/assets/radar-PYXPWWZC-BsRU8Aep.js.br +0 -0
- package/payload/server/public/assets/reduce-BGpN5lgn.js.br +0 -0
- package/payload/server/public/assets/reduce-BGpN5lgn.js.gz +0 -0
- package/payload/server/public/assets/requirementDiagram-MS252O5E-C9QBQxDh.js.br +0 -0
- package/payload/server/public/assets/requirementDiagram-MS252O5E-C9QBQxDh.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-CesEJsuO.js → rotate-ccw-OQD5si8N.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.gz +0 -0
- package/payload/server/public/assets/rough.esm-jSt7UMyi.js.br +0 -0
- package/payload/server/public/assets/rough.esm-jSt7UMyi.js.gz +0 -0
- package/payload/server/public/assets/{routines-DYMYY_r8.js → routines-asrBGZUR.js} +1 -1
- package/payload/server/public/assets/routines-asrBGZUR.js.br +0 -0
- package/payload/server/public/assets/routines-asrBGZUR.js.gz +0 -0
- package/payload/server/public/assets/sankeyDiagram-XADWPNL6-c_y3bvVv.js.br +0 -0
- package/payload/server/public/assets/sankeyDiagram-XADWPNL6-c_y3bvVv.js.gz +0 -0
- package/payload/server/public/assets/sequenceDiagram-FGHM5R23-YwlCBPHE.js.br +0 -0
- package/payload/server/public/assets/sequenceDiagram-FGHM5R23-YwlCBPHE.js.gz +0 -0
- package/payload/server/public/assets/{skills-67jtJ16D.js → skills-D4ECwoxa.js} +1 -1
- package/payload/server/public/assets/skills-D4ECwoxa.js.br +0 -0
- package/payload/server/public/assets/skills-D4ECwoxa.js.gz +0 -0
- package/payload/server/public/assets/src-C2WwTWSY.js.br +0 -0
- package/payload/server/public/assets/src-C2WwTWSY.js.gz +0 -0
- package/payload/server/public/assets/stateDiagram-FHFEXIEX-BFE3e-FM.js.br +0 -0
- package/payload/server/public/assets/stateDiagram-FHFEXIEX-BFE3e-FM.js.gz +0 -0
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CpjB4ykI.js.br +4 -0
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CpjB4ykI.js.gz +0 -0
- package/payload/server/public/assets/{tasks-D2C2a-uk.js → tasks-U3QdbUsO.js} +1 -1
- package/payload/server/public/assets/tasks-U3QdbUsO.js.br +0 -0
- package/payload/server/public/assets/tasks-U3QdbUsO.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-BixTGQAd.js → time-entry-format-BgdTKgYr.js} +1 -1
- package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.gz +0 -0
- package/payload/server/public/assets/timeline-definition-GMOUNBTQ-azqhmRwZ.js.br +0 -0
- package/payload/server/public/assets/timeline-definition-GMOUNBTQ-azqhmRwZ.js.gz +0 -0
- package/payload/server/public/assets/treeView-SZITEDCU-BDzhKA6v.js.br +0 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-epHpdeWa.js.br +0 -0
- package/payload/server/public/assets/{triangle-alert-Co9RWb9q.js → triangle-alert-Ck_3VhT9.js} +1 -1
- package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-7zwf9_w4.js → useCopyFeedback-BdUwyNpa.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CJKjEPNN.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CJKjEPNN.js.gz +0 -0
- package/payload/server/public/assets/{useSubAccountSwitcher-DE4v5_Fz.css → useSubAccountSwitcher-DOWQXDT8.css} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-CZDDiwMD.js → useVoiceRecorder-ChnBrzbZ.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.gz +0 -0
- package/payload/server/public/assets/vennDiagram-DHZGUBPP-Da2RfFs8.js.br +0 -0
- package/payload/server/public/assets/vennDiagram-DHZGUBPP-Da2RfFs8.js.gz +0 -0
- package/payload/server/public/assets/wardley-RL74JXVD-DNSZCrxV.js.br +0 -0
- package/payload/server/public/assets/wardleyDiagram-NUSXRM2D-HdrdBYES.js.br +0 -0
- package/payload/server/public/assets/wardleyDiagram-NUSXRM2D-HdrdBYES.js.gz +0 -0
- package/payload/server/public/assets/{wrench-CjQqdj4P.js → wrench-Cpee3C3J.js} +1 -1
- package/payload/server/public/assets/wrench-Cpee3C3J.js.br +2 -0
- package/payload/server/public/assets/wrench-Cpee3C3J.js.gz +0 -0
- package/payload/server/public/assets/xychartDiagram-5P7HB3ND-DMk6rTcQ.js.br +0 -0
- package/payload/server/public/assets/xychartDiagram-5P7HB3ND-DMk6rTcQ.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +817 -715
- package/payload/server/public/assets/AdminLoginScreens-DKeCt1uf.js +0 -1
- package/payload/server/public/assets/admin-C0-WU1sf.js +0 -1
- package/payload/server/public/assets/agents-DjUvMFTj.js +0 -1
- package/payload/server/public/assets/chat-C0Ps84U5.js +0 -1
- package/payload/server/public/assets/chevron-left-DQ2uNlu9.js +0 -1
- package/payload/server/public/assets/chevron-right-C4QCiRGD.js +0 -1
- package/payload/server/public/assets/clock-BEeQ7JxX.js +0 -1
- package/payload/server/public/assets/data-CGuNfym7.js +0 -1
- /package/payload/server/public/assets/{useSubAccountSwitcher-Bg72hdQ8.js → useSubAccountSwitcher-CJKjEPNN.js} +0 -0
|
@@ -45,10 +45,20 @@ afterEach(() => rmSync(dir, { recursive: true, force: true }))
|
|
|
45
45
|
function fakeClient() {
|
|
46
46
|
const calls: { sql: string; params: unknown[] }[] = []
|
|
47
47
|
const rows = new Set<string>()
|
|
48
|
+
// The pointer is modelled because the flip is read back (Task 1926): a client
|
|
49
|
+
// that always answers "no pointer" would make every push read as superseded.
|
|
50
|
+
let pointer: number | null = null
|
|
48
51
|
return {
|
|
49
52
|
calls,
|
|
50
53
|
async query(sql: string, params: unknown[] = []) {
|
|
51
54
|
calls.push({ sql, params })
|
|
55
|
+
if (/SELECT\s+currentGeneration/i.test(sql)) {
|
|
56
|
+
return pointer === null ? [] : [{ currentGeneration: pointer }]
|
|
57
|
+
}
|
|
58
|
+
if (/INSERT INTO directory_state/i.test(sql)) {
|
|
59
|
+
pointer = Number(params[1])
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
52
62
|
// The open paren is load-bearing: `INSERT INTO directory_state` would
|
|
53
63
|
// otherwise register as a row insert and inflate every count.
|
|
54
64
|
if (/INSERT INTO directory \(/i.test(sql)) {
|
|
@@ -308,15 +318,24 @@ function generationClient() {
|
|
|
308
318
|
})
|
|
309
319
|
return []
|
|
310
320
|
}
|
|
321
|
+
if (/SELECT\s+MAX\(generation\)/i.test(sql)) {
|
|
322
|
+
const mine = rows.filter((r) => r.accountId === String(params[0]))
|
|
323
|
+
return [{ g: mine.length ? Math.max(...mine.map((r) => r.generation)) : null }]
|
|
324
|
+
}
|
|
311
325
|
if (/DELETE FROM directory\b/i.test(sql)) {
|
|
312
326
|
const g = Number(params[1])
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
327
|
+
// The post-flip sweep removes strictly OLDER generations
|
|
328
|
+
// (`generation < ?`, Task 1926). `generation != ?` is the shape it
|
|
329
|
+
// replaced and `generation = ?` was the Task 1923 pre-stage clear; both
|
|
330
|
+
// stay modelled so a regression to either shape is visible here.
|
|
331
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
332
|
+
const n = rows[i].generation
|
|
333
|
+
const hit = /generation\s*<\s*\?/i.test(sql)
|
|
334
|
+
? n < g
|
|
335
|
+
: /generation\s*!=\s*\?/i.test(sql)
|
|
336
|
+
? n !== g
|
|
337
|
+
: n === g
|
|
338
|
+
if (hit) rows.splice(i, 1)
|
|
320
339
|
}
|
|
321
340
|
return []
|
|
322
341
|
}
|
|
@@ -374,12 +393,20 @@ function wedgeableClient(seed: { pointer: number; current?: string[]; orphans: s
|
|
|
374
393
|
rows.push({ accountId, relPath, generation })
|
|
375
394
|
return []
|
|
376
395
|
}
|
|
396
|
+
if (/SELECT\s+MAX\(generation\)/i.test(sql)) {
|
|
397
|
+
const mine = rows.filter((r) => r.accountId === String(params[0]))
|
|
398
|
+
return [{ g: mine.length ? Math.max(...mine.map((r) => r.generation)) : null }]
|
|
399
|
+
}
|
|
377
400
|
if (/DELETE FROM directory\b/i.test(sql)) {
|
|
378
401
|
const g = Number(params[1])
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
402
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
403
|
+
const n = rows[i].generation
|
|
404
|
+
const hit = /generation\s*<\s*\?/i.test(sql)
|
|
405
|
+
? n < g
|
|
406
|
+
: /generation\s*!=\s*\?/i.test(sql)
|
|
407
|
+
? n !== g
|
|
408
|
+
: n === g
|
|
409
|
+
if (hit) rows.splice(i, 1)
|
|
383
410
|
}
|
|
384
411
|
return []
|
|
385
412
|
}
|
|
@@ -395,14 +422,18 @@ function wedgeableClient(seed: { pointer: number; current?: string[]; orphans: s
|
|
|
395
422
|
}
|
|
396
423
|
}
|
|
397
424
|
|
|
398
|
-
describe('a partial stage self-heals (Task 1923)', () => {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
425
|
+
describe('a partial stage self-heals (Task 1923, by the Task 1926 generation claim)', () => {
|
|
426
|
+
// The wedge: a stage that died mid-batch left rows at current + 1 while the
|
|
427
|
+
// pointer stayed at current, so every later run recomputed the SAME target
|
|
428
|
+
// and its first INSERT hit UNIQUE(accountId, relPath, generation). The index
|
|
429
|
+
// froze at the stale generation until an operator hand-cleared the orphans,
|
|
430
|
+
// which is what held gls-data for ~6 h on 2026-07-22.
|
|
431
|
+
//
|
|
432
|
+
// Task 1923 healed that with a DELETE before the stage. Task 1926 heals it by
|
|
433
|
+
// claiming above the orphans instead, which needs no DELETE before the flip
|
|
434
|
+
// at all — and that pre-flip DELETE was the statement a concurrent peer could
|
|
435
|
+
// aim at the live tree.
|
|
436
|
+
it('claims above the orphans rather than colliding with them, and sweeps them after the flip', async () => {
|
|
406
437
|
const c = wedgeableClient({
|
|
407
438
|
pointer: 5,
|
|
408
439
|
current: ['output', 'output/report.pdf', 'quotes', 'quotes/CR2969'],
|
|
@@ -412,21 +443,20 @@ describe('a partial stage self-heals (Task 1923)', () => {
|
|
|
412
443
|
const r = await push(c, { log: (l: string) => lines.push(l) })
|
|
413
444
|
|
|
414
445
|
expect(r.rows).toBe(7)
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
expect(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// pointer names: the clear scopes to `next`, never to a reader-visible row.
|
|
423
|
-
expect(clear).toContain('cleared=3')
|
|
446
|
+
// 7, not 6: the orphans sit at 6, so this run claims the one above them.
|
|
447
|
+
expect(c.pointerValue()).toBe(7)
|
|
448
|
+
// Nothing survives at a generation the pointer does not name: the sweep
|
|
449
|
+
// took both the stale tree at 5 and the orphans at 6.
|
|
450
|
+
expect(c.rows.every((row) => row.generation === 7)).toBe(true)
|
|
451
|
+
// No statement deletes anything before the flip any more.
|
|
452
|
+
expect(lines.some((l) => l.includes('op=stage-clear'))).toBe(false)
|
|
424
453
|
})
|
|
425
454
|
|
|
426
|
-
it('
|
|
455
|
+
it('re-stages onto a fresh generation with no orphans present', async () => {
|
|
427
456
|
const c = wedgeableClient({ pointer: 5, orphans: [] })
|
|
428
457
|
const lines: string[] = []
|
|
429
458
|
await push(c, { log: (l: string) => lines.push(l) })
|
|
459
|
+
expect(c.pointerValue()).toBe(6)
|
|
430
460
|
expect(lines.some((l) => l.includes('op=stage-clear'))).toBe(false)
|
|
431
461
|
})
|
|
432
462
|
})
|
|
@@ -505,3 +535,215 @@ describe('the index replace is atomic', () => {
|
|
|
505
535
|
expect(c.calls.filter((x) => /INSERT|DELETE|directory_state/i.test(x.sql))).toEqual([])
|
|
506
536
|
})
|
|
507
537
|
})
|
|
538
|
+
|
|
539
|
+
// Task 1926. Two pushes of ONE account are reachable: the admin server's 60s
|
|
540
|
+
// loop (pushAllAccounts) and a hand-run `--account <id>` are separate OS
|
|
541
|
+
// processes calling the same pushAccount, so no in-process lock can see both.
|
|
542
|
+
// These tests drive both writers against ONE store and snapshot the reader's
|
|
543
|
+
// view after every statement, so "never a half-built tree" stays a measured
|
|
544
|
+
// property rather than a claim.
|
|
545
|
+
//
|
|
546
|
+
// Statement-level atomicity matches SQLite: each exec() applies whole.
|
|
547
|
+
function sharedStore() {
|
|
548
|
+
const rows: { accountId: string; relPath: string; generation: number }[] = []
|
|
549
|
+
let pointer: number | null = null
|
|
550
|
+
const pointerHistory: number[] = []
|
|
551
|
+
const snapshots: string[][] = []
|
|
552
|
+
const deletesAtPointer: number[] = []
|
|
553
|
+
|
|
554
|
+
const visible = () =>
|
|
555
|
+
pointer === null
|
|
556
|
+
? []
|
|
557
|
+
: rows.filter((r) => r.generation === pointer).map((r) => r.relPath).sort()
|
|
558
|
+
|
|
559
|
+
// The three DELETE shapes this file can issue. Order matters: `!=` and `<`
|
|
560
|
+
// must be tested before `=`, or `generation != ?` would read as an equality.
|
|
561
|
+
const deleteMatcher = (sql: string, g: number) => {
|
|
562
|
+
if (/generation\s*<\s*\?/i.test(sql)) return (n: number) => n < g
|
|
563
|
+
if (/generation\s*!=\s*\?/i.test(sql)) return (n: number) => n !== g
|
|
564
|
+
return (n: number) => n === g
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
rows,
|
|
569
|
+
snapshots,
|
|
570
|
+
deletesAtPointer,
|
|
571
|
+
pointerHistory,
|
|
572
|
+
visible,
|
|
573
|
+
pointerValue: () => pointer,
|
|
574
|
+
seed(generation: number, relPaths: string[]) {
|
|
575
|
+
for (const relPath of relPaths) rows.push({ accountId: 'acc-1', relPath, generation })
|
|
576
|
+
pointer = generation
|
|
577
|
+
pointerHistory.push(generation)
|
|
578
|
+
},
|
|
579
|
+
async exec(sql: string, params: unknown[] = []) {
|
|
580
|
+
const out = await (async () => {
|
|
581
|
+
if (/SELECT\s+currentGeneration/i.test(sql)) {
|
|
582
|
+
return pointer === null ? [] : [{ currentGeneration: pointer }]
|
|
583
|
+
}
|
|
584
|
+
if (/SELECT\s+MAX\(generation\)/i.test(sql)) {
|
|
585
|
+
const mine = rows.filter((r) => r.accountId === String(params[0]))
|
|
586
|
+
return [{ g: mine.length ? Math.max(...mine.map((r) => r.generation)) : null }]
|
|
587
|
+
}
|
|
588
|
+
if (/INSERT INTO directory_state/i.test(sql)) {
|
|
589
|
+
const g = Number(params[1])
|
|
590
|
+
// The upsert's WHERE, modelled: with the guard present the pointer
|
|
591
|
+
// moves forward only. Without it, every flip lands.
|
|
592
|
+
const forwardOnly =
|
|
593
|
+
/excluded\.currentGeneration\s*>\s*directory_state\.currentGeneration/i.test(sql)
|
|
594
|
+
if (pointer === null || !forwardOnly || g > pointer) {
|
|
595
|
+
pointer = g
|
|
596
|
+
pointerHistory.push(g)
|
|
597
|
+
}
|
|
598
|
+
return []
|
|
599
|
+
}
|
|
600
|
+
if (/INSERT INTO directory \(/i.test(sql)) {
|
|
601
|
+
const accountId = String(params[0])
|
|
602
|
+
const relPath = String(params[1])
|
|
603
|
+
const generation = Number(params[6])
|
|
604
|
+
if (
|
|
605
|
+
rows.some(
|
|
606
|
+
(r) => r.accountId === accountId && r.relPath === relPath && r.generation === generation,
|
|
607
|
+
)
|
|
608
|
+
) {
|
|
609
|
+
throw new Error(
|
|
610
|
+
'd1 query failed: HTTP 500 — UNIQUE constraint failed: directory.accountId, directory.relPath, directory.generation',
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
rows.push({ accountId, relPath, generation })
|
|
614
|
+
return []
|
|
615
|
+
}
|
|
616
|
+
if (/DELETE FROM directory\b/i.test(sql)) {
|
|
617
|
+
const g = Number(params[1])
|
|
618
|
+
const matches = deleteMatcher(sql, g)
|
|
619
|
+
// The bug's signature: a DELETE that removes rows at the generation
|
|
620
|
+
// the pointer names is a reader-visible deletion of the live tree.
|
|
621
|
+
if (pointer !== null && matches(pointer) && rows.some((r) => r.generation === pointer)) {
|
|
622
|
+
deletesAtPointer.push(pointer)
|
|
623
|
+
}
|
|
624
|
+
for (let i = rows.length - 1; i >= 0; i--) if (matches(rows[i].generation)) rows.splice(i, 1)
|
|
625
|
+
return []
|
|
626
|
+
}
|
|
627
|
+
if (/SELECT COUNT/i.test(sql)) {
|
|
628
|
+
if (/generation\s*=\s*\?/i.test(sql)) {
|
|
629
|
+
const g = Number(params[1])
|
|
630
|
+
return [{ n: rows.filter((r) => r.generation === g).length }]
|
|
631
|
+
}
|
|
632
|
+
return [{ n: rows.filter((r) => r.generation === pointer).length }]
|
|
633
|
+
}
|
|
634
|
+
return []
|
|
635
|
+
})()
|
|
636
|
+
snapshots.push(visible())
|
|
637
|
+
return out
|
|
638
|
+
},
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** A one-shot rendezvous: `reached` settles when the writer parks, `open()` frees it. */
|
|
643
|
+
function gate() {
|
|
644
|
+
let hit!: () => void
|
|
645
|
+
let open!: () => void
|
|
646
|
+
const reached = new Promise<void>((r) => (hit = r))
|
|
647
|
+
const opened = new Promise<void>((r) => (open = r))
|
|
648
|
+
return { reached, opened, hit: () => hit(), open: () => open() }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** A client that parks once, either before or after the matching statement. */
|
|
652
|
+
function pausingClient(
|
|
653
|
+
store: ReturnType<typeof sharedStore>,
|
|
654
|
+
g: ReturnType<typeof gate>,
|
|
655
|
+
at: { before?: (sql: string) => boolean; after?: (sql: string) => boolean },
|
|
656
|
+
) {
|
|
657
|
+
let armed = true
|
|
658
|
+
const calls: { sql: string; params: unknown[] }[] = []
|
|
659
|
+
return {
|
|
660
|
+
calls,
|
|
661
|
+
async query(sql: string, params: unknown[] = []) {
|
|
662
|
+
calls.push({ sql, params })
|
|
663
|
+
if (armed && at.before?.(sql)) {
|
|
664
|
+
armed = false
|
|
665
|
+
g.hit()
|
|
666
|
+
await g.opened
|
|
667
|
+
}
|
|
668
|
+
const out = await store.exec(sql, params)
|
|
669
|
+
if (armed && at.after?.(sql)) {
|
|
670
|
+
armed = false
|
|
671
|
+
g.hit()
|
|
672
|
+
await g.opened
|
|
673
|
+
}
|
|
674
|
+
return out
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const plainClient = (store: ReturnType<typeof sharedStore>) => ({
|
|
680
|
+
query: (sql: string, params: unknown[] = []) => store.exec(sql, params),
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
describe('two concurrent pushes of one account (Task 1926)', () => {
|
|
684
|
+
// The task file's own interleave. B reads the pointer, then stalls. A stages,
|
|
685
|
+
// flips and sweeps a whole publish. B resumes holding a stale `current`.
|
|
686
|
+
//
|
|
687
|
+
// Before the guard: B recomputed next = current + 1 = the generation the
|
|
688
|
+
// pointer now names, its pre-stage COUNT returned A's live rows, and its
|
|
689
|
+
// DELETE emptied the tree a client was reading.
|
|
690
|
+
it('a peer holding a stale pointer never empties or halves the live tree', async () => {
|
|
691
|
+
const store = sharedStore()
|
|
692
|
+
store.seed(5, ['output', 'output/report.pdf', 'quotes', 'quotes/CR2969'])
|
|
693
|
+
const g = gate()
|
|
694
|
+
const b = pausingClient(store, g, { after: (sql) => /SELECT\s+currentGeneration/i.test(sql) })
|
|
695
|
+
|
|
696
|
+
// Captured rather than awaited bare: B throwing for an unrelated reason
|
|
697
|
+
// would otherwise surface as a passing test.
|
|
698
|
+
const bRun = push(b, { log: () => {} }).catch((e: unknown) => e)
|
|
699
|
+
await g.reached
|
|
700
|
+
await push(plainClient(store), { log: () => {} })
|
|
701
|
+
g.open()
|
|
702
|
+
const bResult = await bRun
|
|
703
|
+
|
|
704
|
+
expect(bResult).not.toBeInstanceOf(Error)
|
|
705
|
+
// 4 is the seeded tree, 7 is a complete walk of the fixture. Any other
|
|
706
|
+
// count is a tree a client could see half-built.
|
|
707
|
+
expect(store.snapshots.every((s) => s.length === 4 || s.length === 7)).toBe(true)
|
|
708
|
+
expect(store.deletesAtPointer).toEqual([])
|
|
709
|
+
expect(store.visible().length).toBe(7)
|
|
710
|
+
// B resumed holding a stale pointer of 5, saw A's rows at 6 in the MAX, and
|
|
711
|
+
// claimed 7. Nothing was staged over and nothing was deleted at a live one.
|
|
712
|
+
expect(store.pointerValue()).toBe(7)
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
// The other order: A stages a whole generation, then stalls before its flip.
|
|
716
|
+
// B starts fresh, sees A's staged rows in the MAX, claims the generation
|
|
717
|
+
// above, and publishes. A wakes holding a generation that is now behind.
|
|
718
|
+
it('a writer overtaken while staging leaves the winner live and says so', async () => {
|
|
719
|
+
const store = sharedStore()
|
|
720
|
+
store.seed(5, ['output', 'output/report.pdf', 'quotes', 'quotes/CR2969'])
|
|
721
|
+
const g = gate()
|
|
722
|
+
const a = pausingClient(store, g, { before: (sql) => /INSERT INTO directory_state/i.test(sql) })
|
|
723
|
+
const lines: string[] = []
|
|
724
|
+
|
|
725
|
+
const aRun = push(a, { log: (l: string) => lines.push(l) })
|
|
726
|
+
await g.reached
|
|
727
|
+
await push(plainClient(store), { log: () => {} })
|
|
728
|
+
g.open()
|
|
729
|
+
const aResult = await aRun
|
|
730
|
+
|
|
731
|
+
expect(store.snapshots.every((s) => s.length === 4 || s.length === 7)).toBe(true)
|
|
732
|
+
expect(store.deletesAtPointer).toEqual([])
|
|
733
|
+
// The pointer never goes backward, which is what keeps the loser harmless.
|
|
734
|
+
expect(store.pointerHistory).toEqual([...store.pointerHistory].sort((x, y) => x - y))
|
|
735
|
+
expect(store.pointerValue()).toBe(7)
|
|
736
|
+
|
|
737
|
+
const contended = lines.find((l) => l.includes('op=stage-contended'))!
|
|
738
|
+
expect(contended).toContain('account=acc-1')
|
|
739
|
+
expect(contended).toContain('generation=6')
|
|
740
|
+
expect(contended).toContain('pointer=7')
|
|
741
|
+
expect(contended).toContain('action=superseded')
|
|
742
|
+
expect(lines.some((l) => l.includes('op=flip'))).toBe(false)
|
|
743
|
+
// No sweep and no verify from the loser: both would act on, and measure,
|
|
744
|
+
// the WINNER's tree and report a failure where nothing failed.
|
|
745
|
+
expect(a.calls.some((c) => /DELETE FROM directory/i.test(c.sql))).toBe(false)
|
|
746
|
+
expect(lines.some((l) => l.includes('op=verify'))).toBe(false)
|
|
747
|
+
expect(aResult.superseded).toBe(true)
|
|
748
|
+
})
|
|
749
|
+
})
|
|
@@ -68,6 +68,27 @@ describe('resolveExposedDirs', () => {
|
|
|
68
68
|
}
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
it('does not read a declared file as a plugin-owned dir', () => {
|
|
72
|
+
// Task 1902 — the plugin-owned region also carries file lines now. This is
|
|
73
|
+
// the client-facing copy of the parser, kept separately from
|
|
74
|
+
// platform/lib/account-schema-regions, so it needs its own case: a file must
|
|
75
|
+
// never enter pluginOwned, and asking to expose one must not expose it. The
|
|
76
|
+
// trailing slash inside the backticks is the discriminator.
|
|
77
|
+
const withFile = [
|
|
78
|
+
'<!-- plugin-owned-dirs:start -->',
|
|
79
|
+
'## Plugin-owned top-level entries',
|
|
80
|
+
'',
|
|
81
|
+
'- `sites/` — Owned by the cloudflare plugin.',
|
|
82
|
+
'- `data-portal.json` — the account data-portal index.',
|
|
83
|
+
'<!-- plugin-owned-dirs:end -->',
|
|
84
|
+
].join('\n')
|
|
85
|
+
const r = resolveExposedDirs(schema({ ontology: ONTOLOGY, pluginOwned: withFile }), [
|
|
86
|
+
'data-portal.json',
|
|
87
|
+
])
|
|
88
|
+
expect(r.pluginOwned).toEqual(['sites'])
|
|
89
|
+
expect(r.exposed).not.toContain('data-portal.json')
|
|
90
|
+
})
|
|
91
|
+
|
|
71
92
|
it('reports a plugin-owned collision and keeps the dir out of exposed', () => {
|
|
72
93
|
const collide = ONTOLOGY.replace('`quotes/`', '`sites/`').replace('Quotation', 'Site')
|
|
73
94
|
const r = resolveExposedDirs(schema({ ontology: collide, pluginOwned: PLUGIN_OWNED }))
|
|
@@ -150,10 +150,11 @@ CREATE UNIQUE INDEX IF NOT EXISTS auth_attempts_scope_window ON auth_attempts (s
|
|
|
150
150
|
-- No backfill follows it: the push replaces every row each cycle.
|
|
151
151
|
--
|
|
152
152
|
-- `generation` is which staging round a row belongs to (Task 1910). The push
|
|
153
|
-
--
|
|
154
|
-
--
|
|
155
|
-
--
|
|
156
|
-
--
|
|
153
|
+
-- claims a generation above every generation the account already holds, writes
|
|
154
|
+
-- the whole tree there, moves the pointer below in ONE forward-only statement,
|
|
155
|
+
-- then deletes strictly older generations (Task 1926). Uniqueness is scoped by
|
|
156
|
+
-- generation because two of them hold the same relPath between the write and
|
|
157
|
+
-- the flip; UNIQUE (accountId, relPath) would reject the staged write outright.
|
|
157
158
|
-- ALTER TABLE directory ADD COLUMN generation INTEGER NOT NULL DEFAULT 0;
|
|
158
159
|
CREATE TABLE IF NOT EXISTS directory (
|
|
159
160
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -236,8 +236,8 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
|
|
|
236
236
|
|
|
237
237
|
| Route | Behaviour |
|
|
238
238
|
|-------|-----------|
|
|
239
|
-
| `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent), its user-created specialists from `plugins/*/agents/*.md`, and its shipped specialists, each row tagged `kind` and `origin` and carrying `risk`, `
|
|
240
|
-
| `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=` or `?origin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, that plugin's user-created specialist; with `?origin=shipped`, the shipped one. Both specialist shapes add `{risk,
|
|
239
|
+
| `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent), its user-created specialists from `plugins/*/agents/*.md`, and its shipped specialists, each row tagged `kind` and `origin` and carrying `risk`, `riskiestTool`, `unresolved` and `disabled`. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped}`. 401 when the session maps to no account. |
|
|
240
|
+
| `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=` or `?origin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, that plugin's user-created specialist; with `?origin=shipped`, the shipped one. Both specialist shapes add `{risk, riskiestTool, unresolved, byTool, disabled}`, where `byTool` is one class per declared tool. Selection is explicit rather than inferred, because a premium file and a user-created specialist under a plugin of the same name produce the same slug. Unknown 404s, 401 as above. |
|
|
241
241
|
| `POST /api/admin/agents/:slug/disable?accountId=` | Shipped agents only, enforced here: a slug present in none of the three shipped directories 404s. Moves the file from `specialists/agents/` to `specialists/agents-disabled/` and records the basename in the 0600 store. Returns `{ok, moved}`; `moved:false` means the agent existed only as a bundled template, so nothing was there to move and the store entry is what stops the next provisioning run delivering it. The store is read strictly before anything moves, so an unreadable store 500s with the file untouched rather than rewriting the file whole from an empty set and dropping every other disabled agent. Same `accountId` contract as delete. |
|
|
242
242
|
| `POST /api/admin/agents/:slug/enable?accountId=` | Covers both of disable's outcomes. Returns `{ok, restored}`: a quarantined file moves back (`restored:true`); a bundled-only agent has nothing to move, so clearing the store entry is the whole job (`restored:false`), which is what stops provisioning withholding it. Treating that second case as "nothing to restore" made disable a one-way door. 404 only when neither directory nor store knows the agent. |
|
|
243
243
|
| `DELETE /api/admin/agents/:slug?accountId=` | Public agents only, on the named validated account. Removes the dir after `deleteAgentProjection`; refuses the `admin` slug (403) and a missing/unknown `accountId` (400) with no write. Loud-fail: a graph-cleanup throw aborts the file removal. |
|
|
@@ -267,10 +267,15 @@ Agents page survived undiagnosed on a multi-account install.
|
|
|
267
267
|
`reason=<graph-cleanup-failed|rm-error>` on failure (files preserved on a
|
|
268
268
|
graph-cleanup throw).
|
|
269
269
|
- `op=classify account=<id8> agent=<name> origin=<shipped|public|specialist>
|
|
270
|
-
risk=<green|amber|red>
|
|
270
|
+
risk=<green|amber|red> riskiestTool=<tool> tools=<n> unresolved=<comma-list|none>
|
|
271
271
|
disabled=<bool>` — one line per classified agent per listing, so it fires on
|
|
272
|
-
every page load. `
|
|
273
|
-
re-deriving it.
|
|
272
|
+
every page load. `riskiestTool` makes a wrong class diagnosable without
|
|
273
|
+
re-deriving it. Its value is the FULL canonical tool name, while the card
|
|
274
|
+
renders only the segment after the plugin prefix. One load
|
|
275
|
+
showing `riskiest tool: memory-update` on the card and
|
|
276
|
+
`riskiestTool=mcp__plugin_memory_memory__memory-update` on this line is the
|
|
277
|
+
confirmation that both derive from one value.
|
|
278
|
+
**A non-empty `unresolved=` is the one to act on**: the agent
|
|
274
279
|
is forced red, and the cause is that the built-in table or the registry
|
|
275
280
|
mapping has drifted, not that the agent changed.
|
|
276
281
|
- `op=risk-surface status=load-failed reason=<msg>` — the plugin registry did
|
|
@@ -283,7 +283,7 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
|
|
|
283
283
|
|
|
284
284
|
`maxy-edge.service` (always-on front door) classifies upstream errors and serves a brand-aware response. There are two distinct user-visible shapes; the right one depends on what failed.
|
|
285
285
|
|
|
286
|
-
**Branded holding page (brand logo + "Starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. The page renders the brand logo (inlined as a base64 data URI at edge boot from `<install>/server/public/brand/<assets.logo>`) and the brand display/body fonts (loaded from fonts.googleapis.com) — both paths bypass the unavailable upstream so the page never makes a same-origin asset fetch. When `brand.logoContainsName` is true the logo replaces the productName text; otherwise the page falls back to "{{productName}} is starting". No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds. Boot-time confirmation that the logo resolved: `[edge] brand=<name> holding-logo=inlined assets-dir=<path>` — `holding-logo=missing` means the logo file wasn't found at `assets-dir`, the page degrades to text-only.
|
|
286
|
+
**Branded holding page (brand logo + "Starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. The page renders the brand logo (inlined as a base64 data URI at edge boot from `<install>/server/public/brand/<assets.logo>`) and the brand display/body fonts (loaded from fonts.googleapis.com) — both paths bypass the unavailable upstream so the page never makes a same-origin asset fetch. When `brand.logoContainsName` is true the logo replaces the productName text; otherwise the page falls back to "{{productName}} is starting". No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] ts=<ISO-8601> upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds. `ts=` is what lets a run of these be read as an outage with a start, an end and a duration, and cross-read against `journalctl --user -u <brand>-code.service`; `path=` carries the endpoint only, never the query string, because this line fires on paths whose query can hold a download signature, an OAuth code, or a customer phone number. Boot-time confirmation that the logo resolved: `[edge] brand=<name> holding-logo=inlined assets-dir=<path>` — `holding-logo=missing` means the logo file wasn't found at `assets-dir`, the page degrades to text-only.
|
|
287
287
|
|
|
288
288
|
**Branded plain-text 502 ("Bad Gateway ({{productName}} unavailable)") — real upstream failure, not cold-start.** Any error class other than `ECONNREFUSED` (timeouts, resets, host-unreachable) returns the existing 502 path. The diagnostic line carries `err-class=other`. Read the log with `tail -200 ~/.maxy/logs/edge.log | rg 'err-class=other'` and check `~/.maxy/logs/server.log` for upstream stack traces — the upstream itself is the source.
|
|
289
289
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scheduling
|
|
3
3
|
description: "Calendar and scheduling — create events, manage appointments, set up recurring triggers."
|
|
4
|
+
account-owned-files: [{"file": "calendar-availability.json", "description": "The booking page's availability config, written by the publish-availability script and read by the live booking page."}]
|
|
4
5
|
tools:
|
|
5
6
|
- name: schedule-event
|
|
6
7
|
publicAllowlist: false
|
|
@@ -10,8 +10,10 @@ REAL_PROJECT_DIR="$(cd "$PLAT_SCRIPTS/.." && pwd)"
|
|
|
10
10
|
REAL_TEMPLATE="$REAL_PROJECT_DIR/templates/account-schema/SCHEMA.md"
|
|
11
11
|
REAL_PREMIUM="$(cd "$REAL_PROJECT_DIR/.." && pwd)/premium-plugins"
|
|
12
12
|
PASS=0; FAIL=0; FAILED=()
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# `--` so a pattern that starts with a hyphen (a region line: "- `x.json` — …")
|
|
14
|
+
# is read as the pattern, not as a grep option.
|
|
15
|
+
assert_grep() { if printf '%s' "$2" | grep -qF -- "$1"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected to find [$1]"); fi; }
|
|
16
|
+
assert_nogrep() { if printf '%s' "$2" | grep -qF -- "$1"; then FAIL=$((FAIL+1)); FAILED+=("$3: expected NOT to find [$1]"); else PASS=$((PASS+1)); fi; }
|
|
15
17
|
assert_eq() { if [ "$1" = "$2" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected [$2] got [$1]"); fi; }
|
|
16
18
|
assert_dir() { if [ -d "$1" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2: expected directory [$1]"); fi; }
|
|
17
19
|
assert_nodir() { if [ -d "$1" ]; then FAIL=$((FAIL+1)); FAILED+=("$2: expected NO directory [$1]"); else PASS=$((PASS+1)); fi; }
|
|
@@ -57,7 +59,7 @@ assert_grep "op=seed-owned-dirs" "$OUT" "sd-seed-log"
|
|
|
57
59
|
assert_grep "added=quoting" "$OUT" "sd-added-quoting"
|
|
58
60
|
ALLOWED="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_SD/SCHEMA.md")"
|
|
59
61
|
assert_grep "quoting" "$ALLOWED" "sd-allowlist-has-quoting"
|
|
60
|
-
assert_grep "Plugin-owned top-level
|
|
62
|
+
assert_grep "Plugin-owned top-level entries" "$(cat "$A_SD/SCHEMA.md")" "sd-descriptive-section"
|
|
61
63
|
assert_grep "plugin-owned-dirs:start" "$(cat "$A_SD/SCHEMA.md")" "sd-marker"
|
|
62
64
|
|
|
63
65
|
# --- idempotency: second merge is byte-identical ---
|
|
@@ -73,7 +75,7 @@ export PROJECT_DIR="$T_MX/platform"
|
|
|
73
75
|
merge_owned_dirs_into_schema "$A_MX" >/dev/null
|
|
74
76
|
ALLOWED_MX="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_MX/SCHEMA.md")"
|
|
75
77
|
assert_nogrep "quoting" "$ALLOWED_MX" "mx-no-quoting"
|
|
76
|
-
assert_nogrep "Plugin-owned top-level
|
|
78
|
+
assert_nogrep "Plugin-owned top-level entries" "$(cat "$A_MX/SCHEMA.md")" "mx-no-section"
|
|
77
79
|
|
|
78
80
|
# --- platform plugins declare pages (storage-broker) + archive (memory) ---
|
|
79
81
|
# Task 1891: these two plugin-owned root dirs must land in the allowed block so
|
|
@@ -287,5 +289,111 @@ B_P="$(shasum "$A_P/SCHEMA.md" | awk '{print $1}')"
|
|
|
287
289
|
merge_owned_dirs_into_schema "$A_P" >/dev/null
|
|
288
290
|
assert_eq "$(shasum "$A_P/SCHEMA.md" | awk '{print $1}')" "$B_P" "prune-idempotent"
|
|
289
291
|
|
|
292
|
+
# --- account-owned-files: a declared file joins the same allowed block ---
|
|
293
|
+
# The block is a flat list of names and already carries SCHEMA.md and
|
|
294
|
+
# account.json, so a file needs no second surface. A second plugin declares a
|
|
295
|
+
# malformed value: its key is skipped and the valid declaration still merges.
|
|
296
|
+
TF="$(mktemp -d)"
|
|
297
|
+
mkdir -p "$TF/platform/plugins/filer" "$TF/platform/plugins/broken" \
|
|
298
|
+
"$TF/platform/config" "$TF/platform/templates/account-schema"
|
|
299
|
+
cp "$REAL_TEMPLATE" "$TF/platform/templates/account-schema/SCHEMA.md"
|
|
300
|
+
printf '{"installDir":"files-brand","shipsPremiumBundles":[]}\n' > "$TF/platform/config/brand.json"
|
|
301
|
+
printf -- '---\nname: filer\naccount-owned-files: [{"file": "x.json", "description": "Filer index."}]\n---\n' \
|
|
302
|
+
> "$TF/platform/plugins/filer/PLUGIN.md"
|
|
303
|
+
printf -- '---\nname: broken\naccount-owned-files: [{"file": "y.json"\n---\n' \
|
|
304
|
+
> "$TF/platform/plugins/broken/PLUGIN.md"
|
|
305
|
+
A_TF="$(mktemp -d)"; cp "$TF/platform/templates/account-schema/SCHEMA.md" "$A_TF/SCHEMA.md"
|
|
306
|
+
export PROJECT_DIR="$TF/platform"
|
|
307
|
+
OUT_TF="$(merge_owned_dirs_into_schema "$A_TF")"
|
|
308
|
+
ALLOWED_TF="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_TF/SCHEMA.md")"
|
|
309
|
+
assert_grep "x.json" "$ALLOWED_TF" "files-allowlist-has-x"
|
|
310
|
+
assert_nogrep "y.json" "$ALLOWED_TF" "files-malformed-skipped"
|
|
311
|
+
assert_grep "added=x.json" "$OUT_TF" "files-added-logged"
|
|
312
|
+
# A file renders without the trailing slash a dir gets; three parsers read that
|
|
313
|
+
# slash to tell a bucket from anything else.
|
|
314
|
+
assert_grep '- `x.json` — Filer index.' "$(cat "$A_TF/SCHEMA.md")" "files-region-no-slash"
|
|
315
|
+
assert_nogrep '- `x.json/`' "$(cat "$A_TF/SCHEMA.md")" "files-region-not-a-dir"
|
|
316
|
+
RC_TF="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_TF")"
|
|
317
|
+
assert_grep "ownedFile=x.json present=true" "$RC_TF" "files-reconcile-line"
|
|
318
|
+
B_TF="$(shasum "$A_TF/SCHEMA.md" | awk '{print $1}')"
|
|
319
|
+
merge_owned_dirs_into_schema "$A_TF" >/dev/null
|
|
320
|
+
assert_eq "$(shasum "$A_TF/SCHEMA.md" | awk '{print $1}')" "$B_TF" "files-idempotent"
|
|
321
|
+
|
|
322
|
+
# A dir declaration keeps its slash and its ownedDir= reconcile line.
|
|
323
|
+
TD="$(mktemp -d)"
|
|
324
|
+
mkdir -p "$TD/platform/plugins/dirred" "$TD/platform/config" "$TD/platform/templates/account-schema"
|
|
325
|
+
cp "$REAL_TEMPLATE" "$TD/platform/templates/account-schema/SCHEMA.md"
|
|
326
|
+
printf '{"installDir":"dirs-brand","shipsPremiumBundles":[]}\n' > "$TD/platform/config/brand.json"
|
|
327
|
+
printf -- '---\nname: dirred\naccount-owned-dirs: [{"dir": "widgets", "description": "Widget workspace."}]\n---\n' \
|
|
328
|
+
> "$TD/platform/plugins/dirred/PLUGIN.md"
|
|
329
|
+
A_TD="$(mktemp -d)"; cp "$TD/platform/templates/account-schema/SCHEMA.md" "$A_TD/SCHEMA.md"
|
|
330
|
+
export PROJECT_DIR="$TD/platform"
|
|
331
|
+
merge_owned_dirs_into_schema "$A_TD" >/dev/null
|
|
332
|
+
assert_grep '- `widgets/` — Widget workspace.' "$(cat "$A_TD/SCHEMA.md")" "dirs-region-keeps-slash"
|
|
333
|
+
RC_TD="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_TD")"
|
|
334
|
+
assert_grep "ownedDir=widgets present=true" "$RC_TD" "dirs-reconcile-line"
|
|
335
|
+
|
|
336
|
+
# --- a malformed ELEMENT skips its whole key, not just that element ---
|
|
337
|
+
# json.loads succeeds and the first entry is well-formed, so an append-as-you-go
|
|
338
|
+
# parse would merge a.json and silently drop c.json. A half-merged declaration is
|
|
339
|
+
# worse than none: the dropped name reports as a stray forever with no line
|
|
340
|
+
# anywhere saying its declaration failed.
|
|
341
|
+
TP="$(mktemp -d)"
|
|
342
|
+
mkdir -p "$TP/platform/plugins/partial" "$TP/platform/plugins/good" \
|
|
343
|
+
"$TP/platform/config" "$TP/platform/templates/account-schema"
|
|
344
|
+
cp "$REAL_TEMPLATE" "$TP/platform/templates/account-schema/SCHEMA.md"
|
|
345
|
+
printf '{"installDir":"partial-brand","shipsPremiumBundles":[]}\n' > "$TP/platform/config/brand.json"
|
|
346
|
+
printf -- '---\nname: partial\naccount-owned-files: [{"file": "a.json"}, "b.json", {"file": "c.json"}]\n---\n' \
|
|
347
|
+
> "$TP/platform/plugins/partial/PLUGIN.md"
|
|
348
|
+
printf -- '---\nname: good\naccount-owned-files: [{"file": "d.json", "description": "Good."}]\n---\n' \
|
|
349
|
+
> "$TP/platform/plugins/good/PLUGIN.md"
|
|
350
|
+
A_TP="$(mktemp -d)"; cp "$TP/platform/templates/account-schema/SCHEMA.md" "$A_TP/SCHEMA.md"
|
|
351
|
+
export PROJECT_DIR="$TP/platform"
|
|
352
|
+
merge_owned_dirs_into_schema "$A_TP" >/dev/null
|
|
353
|
+
ALLOWED_TP="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_TP/SCHEMA.md")"
|
|
354
|
+
assert_nogrep "a.json" "$ALLOWED_TP" "partial-first-entry-skipped"
|
|
355
|
+
assert_nogrep "c.json" "$ALLOWED_TP" "partial-later-entry-skipped"
|
|
356
|
+
assert_grep "d.json" "$ALLOWED_TP" "partial-sibling-plugin-still-merges"
|
|
357
|
+
|
|
358
|
+
# --- a name carrying a path separator is refused ---
|
|
359
|
+
# The trailing slash inside the backticks is what tells a bucket from a file in
|
|
360
|
+
# the descriptive region, and the fence entry must match a readdir name. A
|
|
361
|
+
# declared "foo/" would render as a bucket line and never match anything.
|
|
362
|
+
TS_="$(mktemp -d)"
|
|
363
|
+
mkdir -p "$TS_/platform/plugins/slashed" "$TS_/platform/config" "$TS_/platform/templates/account-schema"
|
|
364
|
+
cp "$REAL_TEMPLATE" "$TS_/platform/templates/account-schema/SCHEMA.md"
|
|
365
|
+
printf '{"installDir":"slash-brand","shipsPremiumBundles":[]}\n' > "$TS_/platform/config/brand.json"
|
|
366
|
+
printf -- '---\nname: slashed\naccount-owned-files: [{"file": "foo/", "description": "Bad."}]\naccount-owned-dirs: [{"dir": "bar/baz", "description": "Bad."}]\n---\n' \
|
|
367
|
+
> "$TS_/platform/plugins/slashed/PLUGIN.md"
|
|
368
|
+
A_TS="$(mktemp -d)"; cp "$TS_/platform/templates/account-schema/SCHEMA.md" "$A_TS/SCHEMA.md"
|
|
369
|
+
export PROJECT_DIR="$TS_/platform"
|
|
370
|
+
merge_owned_dirs_into_schema "$A_TS" >/dev/null
|
|
371
|
+
assert_nogrep "foo/" "$(cat "$A_TS/SCHEMA.md")" "slash-file-refused"
|
|
372
|
+
assert_nogrep "bar/baz" "$(cat "$A_TS/SCHEMA.md")" "slash-dir-refused"
|
|
373
|
+
|
|
374
|
+
# --- shipped declarations reach an account's allowed block ---
|
|
375
|
+
# Symlink the real plugin dirs so the case exercises the shipped PLUGIN.md
|
|
376
|
+
# frontmatter, the same way the Task 1891 pages/archive case does.
|
|
377
|
+
T_SHIP="$(make_tree '[]')"
|
|
378
|
+
for _p in business-assistant cloudflare scheduling admin; do
|
|
379
|
+
ln -s "$REAL_PROJECT_DIR/plugins/$_p" "$T_SHIP/platform/plugins/$_p"
|
|
380
|
+
done
|
|
381
|
+
A_SHIP="$(make_account "$T_SHIP")"
|
|
382
|
+
export PROJECT_DIR="$T_SHIP/platform"
|
|
383
|
+
merge_owned_dirs_into_schema "$A_SHIP" >/dev/null
|
|
384
|
+
ALLOWED_SHIP="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_SHIP/SCHEMA.md")"
|
|
385
|
+
# The four channel/session files are written by the session manager and the UI
|
|
386
|
+
# server on every brand, so they are declared on admin, which no brand excludes.
|
|
387
|
+
# realagent-code and property-administrators exclude the telegram plugin from
|
|
388
|
+
# their payload (brands/*/brand.json) while still constructing the telegram store
|
|
389
|
+
# (services/claude-session-manager/src/index.ts), so a declaration on the telegram
|
|
390
|
+
# plugin would leave those two brands reporting a permanent false stray. This
|
|
391
|
+
# tree deliberately omits whatsapp and telegram to prove the reach.
|
|
392
|
+
for _n in e-sign data-portal.json calendar-availability.json wa-channel-bindings.json \
|
|
393
|
+
telegram-channel-bindings.json webchat-channel-bindings.json \
|
|
394
|
+
canonical-webchat-session.json session-titles.json agents-disabled.json; do
|
|
395
|
+
assert_grep "$_n" "$ALLOWED_SHIP" "ship-allow-$_n"
|
|
396
|
+
done
|
|
397
|
+
|
|
290
398
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
291
399
|
if [ "$FAIL" -ne 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
|