create-workframe 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +1 -1
  2. package/docs/security.md +85 -0
  3. package/package.json +1 -1
  4. package/scripts/apply-update-workframe.sh +7 -6
  5. package/scripts/sync-canonical-to-package.mjs +8 -0
  6. package/scripts/verify-public-deploy.sh +59 -3
  7. package/workframe-api/openrouter_catalog.py +4 -4
  8. package/workframe-api/package.json +2 -2
  9. package/workframe-api/profile_config_yaml.py +126 -0
  10. package/workframe-api/route_registry.py +330 -0
  11. package/workframe-api/run-typecheck.mjs +25 -0
  12. package/workframe-api/server.py +1523 -1392
  13. package/workframe-api/test_model_surface_consistency.py +116 -0
  14. package/workframe-api/test_profile_model_yaml.py +99 -0
  15. package/workframe-api/test_public_routes.py +41 -0
  16. package/workframe-api/test_route_registry.py +109 -0
  17. package/workframe-api/time-bind-steps.py +75 -0
  18. package/workframe-api/time-perf.py +81 -0
  19. package/workframe-api/time-warm.py +107 -0
  20. package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-B0OFRGmJ.js} +1 -1
  21. package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +1 -0
  22. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DeBNltHS.js} +1 -1
  23. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-BDhCHu7I.js} +1 -1
  24. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-olcDYvtI.js} +1 -1
  25. package/workframe-ui/public/assets/channel-fNHbDpV4.js +1 -0
  26. package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-D0obCBn_.js} +1 -1
  27. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DX93f-ZZ.js} +1 -1
  28. package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-BX9B5wUs.js} +1 -1
  29. package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-C4DGc6RO.js} +1 -1
  30. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-Crlja9Lf.js} +1 -1
  31. package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-JYSm3szI.js} +1 -1
  32. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-_KslxVEl.js} +1 -1
  33. package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-CpTO-jU8.js} +1 -1
  34. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-JGUkCmbI.js} +1 -1
  35. package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-BscBwGaC.js} +1 -1
  36. package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-5ZTZ0bpX.js} +1 -1
  37. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-DxvitYbW.js} +1 -1
  38. package/workframe-ui/public/assets/{chunk-LZXEDZCA-CsRdBqUB.js → chunk-LZXEDZCA-DMfdMUwz.js} +2 -2
  39. package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-Cayl0iV9.js} +1 -1
  40. package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-_7dyBR5G.js} +1 -1
  41. package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O--xTpD0yi.js} +1 -1
  42. package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +1 -0
  43. package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +1 -0
  44. package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-CwxJQaeK.js} +1 -1
  45. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +1 -0
  46. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +1 -0
  47. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-CCspENYv.js} +1 -1
  48. package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Dv8V6R60.js} +1 -1
  49. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-Da48hFPT.js} +1 -1
  50. package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-dTihc7-3.js} +1 -1
  51. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-D_Jqu699.js} +1 -1
  52. package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-Bt8PtEaZ.js} +1 -1
  53. package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-BtmPjlh0.js} +1 -1
  54. package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-Cz2turIT.js} +1 -1
  55. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-BxMGCflX.js} +1 -1
  56. package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +1 -0
  57. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-ivyroIZt.js} +1 -1
  58. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-B9llwShx.js} +1 -1
  59. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-BFQHk4bw.js} +1 -1
  60. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-ursegLbc.js} +1 -1
  61. package/workframe-ui/public/assets/index-8CuZDEIG.css +1 -0
  62. package/workframe-ui/public/assets/index-xS9lFekI.js +129 -0
  63. package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-DQpifAsB.js} +1 -1
  64. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-C16XP2Q7.js} +1 -1
  65. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-CiJoz7rP.js} +1 -1
  66. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-CVwEvvUL.js} +1 -1
  67. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-CWQ6hX5i.js} +1 -1
  68. package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-CgQsmU35.js} +1 -1
  69. package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-Bxbc77dL.js} +1 -1
  70. package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-C_jMeF0a.js} +2 -2
  71. package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-4RKV9MZP.js} +3 -3
  72. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-DZ7y7Sz0.js} +1 -1
  73. package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-B9h2I7Vq.js} +1 -1
  74. package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-B2mP5uHm.js} +1 -1
  75. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-DyYKyKiP.js} +1 -1
  76. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-CL_6nej-.js} +1 -1
  77. package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-CNKNDQ7S.js} +1 -1
  78. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-eWU_mhFa.js} +1 -1
  79. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-DrjcXvEQ.js} +1 -1
  80. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-BHWR1xTJ.js} +1 -1
  81. package/workframe-ui/public/assets/{src-CY_By-r7.js → src-DfJB8kV1.js} +1 -1
  82. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-BGJbSd3I.js} +1 -1
  83. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +1 -0
  84. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-DkwoCjiZ.js} +1 -1
  85. package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-GxfhiqoW.js} +1 -1
  86. package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-DxrOsars.js} +1 -1
  87. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-BsWhpUfr.js} +1 -1
  88. package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-CrFZWMHS.js} +1 -1
  89. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DV8Xpf5I.js} +1 -1
  90. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-DqMqnwdZ.js} +1 -1
  91. package/workframe-ui/public/index.html +4 -5
  92. package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
  93. package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
  94. package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
  95. package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
  96. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
  97. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
  98. package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
  99. package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
  100. package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
  101. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """Regression tests for model save/provider surface (WF-034).
3
+
4
+ Run: python services/workframe-api/test_model_surface_consistency.py
5
+ """
6
+ import importlib.util
7
+ import os
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ ROOT = Path(__file__).resolve().parent
13
+ tmpdir = tempfile.mkdtemp(prefix="wf-model-surface-")
14
+ os.environ["WORKFRAME_AGENTS_DIR"] = tmpdir
15
+ os.environ["WORKFRAME_PROJECT"] = "ModelSurfaceTest"
16
+ os.environ["NATIVE_PROFILE"] = "surface-test-agent"
17
+
18
+ spec = importlib.util.spec_from_file_location("wf_server_model_surface", ROOT / "server.py")
19
+ server = importlib.util.module_from_spec(spec)
20
+ sys.modules["wf_server_model_surface"] = server
21
+ spec.loader.exec_module(server)
22
+
23
+ PROF = "surface-test-agent"
24
+ prof_dir = server._profile_dir(PROF)
25
+ prof_dir.mkdir(parents=True, exist_ok=True)
26
+ (prof_dir / "config.yaml").write_text(
27
+ "model:\n default: gpt-5.4-medium\n provider: openai-codex\n",
28
+ encoding="utf-8",
29
+ )
30
+ cfg_path = prof_dir / "config.yaml"
31
+ before = cfg_path.read_bytes()
32
+
33
+ # A: GET models must not mutate config.yaml
34
+ server.hermes_models(PROF, user_id="", workspace_id="")
35
+ assert cfg_path.read_bytes() == before, "hermes_models must not rewrite config on read"
36
+
37
+ # B: reconcile idempotent when routing already matches codex oauth
38
+ assert server._profile_routing_matches_billing(PROF, "codex")
39
+ assert server._reconcile_profile_llm_for_user(PROF, "user-1", "") is False
40
+
41
+ # C: billing resolution honors prefer= when model exists in that catalog
42
+ connected = {"codex", "openrouter"}
43
+ assert server._resolve_billing_provider_for_model(
44
+ "gpt-5.4-medium", connected, prefer="codex",
45
+ ) == "codex"
46
+ assert server._resolve_billing_provider_for_model(
47
+ "openrouter/owl-alpha", connected, prefer="openrouter",
48
+ ) == "openrouter"
49
+ assert server._resolve_billing_provider_for_model(
50
+ "openai-codex/gpt-5.4-mini", {"codex", "openrouter"},
51
+ ) == "codex"
52
+
53
+ # D: set with explicit billing_provider returns billing id in response
54
+ applied = server.hermes_model_set(
55
+ PROF, "gpt-5.4-medium", user_id="", workspace_id="", billing_provider="codex",
56
+ )
57
+ assert applied.get("ok"), applied
58
+ assert applied.get("billing_provider") == "codex"
59
+ assert applied.get("provider") == "codex"
60
+
61
+ # E: reconcile picks model billing over stale openrouter proxy yaml
62
+ (prof_dir / "config.yaml").write_text(
63
+ "model:\n default: openai-codex/gpt-5.4-mini\n provider: custom\n"
64
+ " base_url: http://workframe-api:8080/internal/llm/openrouter/v1\n",
65
+ encoding="utf-8",
66
+ )
67
+ _orig_picker = server._user_llm_providers_for_picker
68
+ _orig_can = server._user_can_use_llm
69
+ _orig_sync = server._sync_oauth_llm_to_profile
70
+ server._user_llm_providers_for_picker = lambda uid: {"codex"} if uid == "user-1" else set()
71
+ server._user_can_use_llm = lambda uid, ws, prov: uid == "user-1" and prov == "codex"
72
+ server._sync_oauth_llm_to_profile = lambda *a, **k: None
73
+ try:
74
+ assert server._reconcile_profile_llm_for_user(PROF, "user-1", "") is True
75
+ block = server._read_model_block(PROF)
76
+ assert block.get("provider") == "openai-codex"
77
+ assert block.get("default") == "gpt-5.4-mini"
78
+ assert not block.get("base_url")
79
+ finally:
80
+ server._user_llm_providers_for_picker = _orig_picker
81
+ server._user_can_use_llm = _orig_can
82
+ server._sync_oauth_llm_to_profile = _orig_sync
83
+
84
+ # F: runtime disk wins over template in _read_model_block
85
+ runtime_prof = "u-test-user-surface-test-agent"
86
+ runtime_dir = server._profile_dir(runtime_prof)
87
+ runtime_dir.mkdir(parents=True, exist_ok=True)
88
+ (runtime_dir / "config.yaml").write_text(
89
+ "model:\n default: gpt-5.4-mini\n provider: openai-codex\n",
90
+ encoding="utf-8",
91
+ )
92
+ block = server._read_model_block(runtime_prof)
93
+ assert block.get("default") == "gpt-5.4-mini", block
94
+ assert block.get("provider") == "openai-codex", block
95
+
96
+ # G: template model save mirrors to runtime (same model id, not stale template re-read)
97
+ runtime_prof = "u-test-user2-surface-test-agent"
98
+ runtime_dir = server._profile_dir(runtime_prof)
99
+ runtime_dir.mkdir(parents=True, exist_ok=True)
100
+ (runtime_dir / "config.yaml").write_text(
101
+ "model:\n default: gpt-5.4-medium\n provider: openai-codex\n",
102
+ encoding="utf-8",
103
+ )
104
+ _orig_restart = server._restart_runtime_profile_gateway
105
+ server._restart_runtime_profile_gateway = lambda *a, **k: None
106
+ try:
107
+ applied = server.hermes_model_set(
108
+ PROF, "gpt-5.4-mini", user_id="test-user2", workspace_id="", billing_provider="codex",
109
+ )
110
+ finally:
111
+ server._restart_runtime_profile_gateway = _orig_restart
112
+ assert applied.get("ok"), applied
113
+ runtime_block = server._parse_model_block_from_disk(runtime_prof)
114
+ assert runtime_block.get("default") == "gpt-5.4-mini", runtime_block
115
+
116
+ print("model surface consistency self-check ok")
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """Coalesce corrupt duplicate model blocks + profile_config_yaml writer (WF-033)."""
3
+ import importlib.util
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+ ROOT = Path(__file__).resolve().parent
12
+ os.environ.setdefault("WORKFRAME_AGENTS_DIR", tempfile.mkdtemp(prefix="wf-yaml-test-"))
13
+ os.environ.setdefault("WORKFRAME_PROJECT", "YamlTest")
14
+ os.environ.setdefault("NATIVE_PROFILE", "yaml-test-agent")
15
+
16
+ spec = importlib.util.spec_from_file_location("wf_server_yaml_test", ROOT / "server.py")
17
+ server = importlib.util.module_from_spec(spec)
18
+ sys.modules["wf_server_yaml_test"] = server
19
+ spec.loader.exec_module(server)
20
+
21
+ import profile_config_yaml # noqa: E402
22
+
23
+ BROKEN = """model:
24
+ default: gpt-5.4-medium
25
+ provider: openai-codex
26
+ model:
27
+ default: gpt-5.4-medium
28
+ - provider: openai-codex
29
+ fallback_providers:
30
+ - provider: openai-codex
31
+ model: gpt-5.4-mini
32
+ toolsets:
33
+ """
34
+
35
+ prof = "yaml-test-agent"
36
+ prof_dir = server._profile_dir(prof)
37
+ prof_dir.mkdir(parents=True, exist_ok=True)
38
+ cfg_path = prof_dir / "config.yaml"
39
+ cfg_path.write_text(BROKEN, encoding="utf-8")
40
+
41
+ server._normalize_profile_config_yaml(prof)
42
+ fixed = cfg_path.read_text(encoding="utf-8")
43
+ assert fixed.count("\nmodel:\n") + fixed.count("model:\n") <= 2, fixed
44
+ assert " - provider:" not in fixed.split("fallback_providers:")[0], fixed
45
+ parsed = yaml.safe_load(fixed)
46
+ assert isinstance(parsed.get("model"), dict), parsed
47
+ assert parsed["model"]["default"] == "gpt-5.4-medium"
48
+ assert parsed["model"]["provider"] == "openai-codex"
49
+ assert len(parsed.get("fallback_providers") or []) == 1
50
+ assert "toolsets" in parsed
51
+
52
+ # Round-trip via update_model_surface preserves non-model keys
53
+ round_prof = "yaml-round-trip"
54
+ round_dir = server._profile_dir(round_prof)
55
+ round_dir.mkdir(parents=True, exist_ok=True)
56
+ round_path = round_dir / "config.yaml"
57
+ round_path.write_text(
58
+ "# picker note\nmodel:\n default: old-model\n provider: openrouter\ncron: []\n",
59
+ encoding="utf-8",
60
+ )
61
+ profile_config_yaml.update_model_surface(
62
+ round_path, default="new-model", provider="custom", base_url="http://proxy/v1",
63
+ )
64
+ rt = yaml.safe_load(round_path.read_text(encoding="utf-8"))
65
+ assert rt["model"]["default"] == "new-model"
66
+ assert rt["model"]["provider"] == "custom"
67
+ assert rt["model"]["base_url"] == "http://proxy/v1"
68
+ assert rt.get("cron") == []
69
+ assert "# picker note" not in round_path.read_text(encoding="utf-8") # comments not preserved
70
+
71
+ # Missing file: writer creates parent dirs + config
72
+ missing_prof = "yaml-missing"
73
+ missing_dir = server._profile_dir(missing_prof)
74
+ missing_path = missing_dir / "config.yaml"
75
+ if missing_path.is_file():
76
+ missing_path.unlink()
77
+ profile_config_yaml.update_model_surface(
78
+ missing_path, default="gpt-5.4-mini", provider="openai-codex",
79
+ )
80
+ assert missing_path.is_file()
81
+ loaded = yaml.safe_load(missing_path.read_text(encoding="utf-8"))
82
+ assert loaded["model"]["default"] == "gpt-5.4-mini"
83
+
84
+ # Coalesce flag drops duplicate blocks
85
+ dup_path = round_dir / "dup.yaml"
86
+ dup_path.write_text(BROKEN, encoding="utf-8")
87
+ profile_config_yaml.update_model_surface(
88
+ dup_path,
89
+ default="gpt-5.4-medium",
90
+ provider="openai-codex",
91
+ fallback_chain=[{"provider": "openai-codex", "model": "gpt-5.4-mini"}],
92
+ coalesce=True,
93
+ )
94
+ dup_text = dup_path.read_text(encoding="utf-8")
95
+ assert dup_text.count("\nmodel:\n") + dup_text.count("model:\n") <= 2, dup_text
96
+ dup_parsed = yaml.safe_load(dup_text)
97
+ assert dup_parsed["model"]["default"] == "gpt-5.4-medium"
98
+
99
+ print("profile model yaml coalesce self-check ok")
@@ -0,0 +1,41 @@
1
+ """ponytail self-check: session-less GET policy (WF-031).
2
+
3
+ Run: python services/workframe-api/test_public_routes.py
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import server # noqa: E402
11
+
12
+ ALWAYS = "/api/health"
13
+ DATA = "/api/snapshot"
14
+ META = "/api/meta"
15
+
16
+
17
+ def allowed(path: str, mode: str, dev: bool = False) -> bool:
18
+ return server._sessionless_get_allowed(path, deployment_mode=mode, dev_local_unsafe=dev)
19
+
20
+
21
+ # Always-public routes work in every deployment mode.
22
+ assert allowed(META, "public_multi_user")
23
+ assert allowed(ALWAYS, "trusted_team")
24
+ assert allowed("/api/public/branding/logo", "public_multi_user")
25
+
26
+ # Data routes: blocked for multi-user modes without DEV_LOCAL_UNSAFE.
27
+ assert not allowed(DATA, "public_multi_user")
28
+ assert not allowed(DATA, "trusted_team")
29
+ assert not allowed("/api/chat/messages", "public_multi_user")
30
+ assert not allowed("/api/files/list", "trusted_team")
31
+
32
+ # Data routes: allowed for single_user_local or dev unsafe.
33
+ assert allowed(DATA, "single_user_local")
34
+ assert allowed(DATA, "public_multi_user", dev=True)
35
+ assert allowed("/api/board", "single_user_local")
36
+
37
+ # _is_public_get composes static/UI paths.
38
+ assert server._is_public_get("/")
39
+ assert server._is_public_get("/assets/app.js")
40
+
41
+ print("public routes self-check ok")
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ """Route registry audit (WF-037).
3
+
4
+ Run: python services/workframe-api/test_route_registry.py
5
+ """
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ ROOT = Path(__file__).resolve().parent
11
+ sys.path.insert(0, str(ROOT))
12
+
13
+ import route_registry # noqa: E402
14
+
15
+ # Auth level coverage
16
+ assert route_registry.resolve_auth_level("GET", "/api/health") == route_registry.AuthLevel.PUBLIC
17
+ assert route_registry.resolve_auth_level("GET", "/api/meta") == route_registry.AuthLevel.PUBLIC
18
+ assert route_registry.resolve_auth_level("GET", "/api/snapshot") == route_registry.AuthLevel.SINGLE_USER_GET
19
+ assert route_registry.resolve_auth_level("POST", "/api/auth/start") == route_registry.AuthLevel.AUTH_FLOW
20
+ assert route_registry.resolve_auth_level("GET", "/api/me") == route_registry.AuthLevel.SESSION
21
+ assert route_registry.resolve_auth_level("GET", "/api/activity/detail") == route_registry.AuthLevel.SESSION
22
+
23
+ # Unknown /api paths require session (deny when anonymous)
24
+ assert route_registry.resolve_auth_level("GET", "/api/not-a-real-handler") == route_registry.AuthLevel.SESSION
25
+ assert not route_registry.authorize_request(
26
+ "GET",
27
+ "/api/not-a-real-handler",
28
+ "",
29
+ deployment_mode="trusted_team",
30
+ dev_local_unsafe=False,
31
+ install_window_open=False,
32
+ validate_session=lambda _s: None,
33
+ attach_user=lambda _u: None,
34
+ )
35
+
36
+ # ROUTES table: auth tier matches resolve_auth_level
37
+ auth_mismatch: list[str] = []
38
+ for route in route_registry.ROUTES:
39
+ resolved = route_registry.resolve_auth_level(route.method, route.path)
40
+ if resolved != route.auth:
41
+ auth_mismatch.append(f"{route.method} {route.path}: table={route.auth} resolved={resolved}")
42
+ assert not auth_mismatch, f"route auth mismatch: {auth_mismatch}"
43
+
44
+ # ROUTES handlers exist on server.Handler
45
+ server_src = (ROOT / "server.py").read_text(encoding="utf-8")
46
+ missing_handlers: list[str] = []
47
+ for route in route_registry.ROUTES:
48
+ if f"def {route.handler}(" not in server_src:
49
+ missing_handlers.append(f"{route.method} {route.path} -> {route.handler}")
50
+ assert not missing_handlers, f"missing handler methods: {missing_handlers}"
51
+
52
+ # Remaining if-chain GET paths still map to a known auth tier
53
+ handler_paths = set(re.findall(r'if path == "(/api/[^"]+)"', server_src))
54
+ handler_paths |= set(re.findall(r"if path == \'(/api/[^\']+)\'", server_src))
55
+ unmapped: list[str] = []
56
+ for p in sorted(handler_paths):
57
+ level = route_registry.resolve_auth_level("GET", p)
58
+ if level not in (
59
+ route_registry.AuthLevel.PUBLIC,
60
+ route_registry.AuthLevel.SINGLE_USER_GET,
61
+ route_registry.AuthLevel.SESSION,
62
+ route_registry.AuthLevel.INSTALL,
63
+ route_registry.AuthLevel.AUTH_FLOW,
64
+ ):
65
+ unmapped.append(p)
66
+ assert not unmapped, f"unmapped auth levels: {unmapped}"
67
+
68
+ # trusted_team: data GETs need session (not anonymous), not a hard deny
69
+ assert not route_registry.authorize_request(
70
+ "GET",
71
+ "/api/agents",
72
+ "",
73
+ deployment_mode="trusted_team",
74
+ dev_local_unsafe=False,
75
+ install_window_open=False,
76
+ validate_session=lambda _s: None,
77
+ attach_user=lambda _u: None,
78
+ )
79
+ assert route_registry.authorize_request(
80
+ "GET",
81
+ "/api/agents",
82
+ "sid-ok",
83
+ deployment_mode="trusted_team",
84
+ dev_local_unsafe=False,
85
+ install_window_open=False,
86
+ validate_session=lambda s: {"user_id": "u1"} if s == "sid-ok" else None,
87
+ attach_user=lambda _u: None,
88
+ )
89
+
90
+ data_read_get = [r for r in route_registry.ROUTES if r.method == "GET"]
91
+ assert len(data_read_get) == 36
92
+ registry_post = [r for r in route_registry.ROUTES if r.method == "POST"]
93
+ assert len(registry_post) == 25
94
+
95
+ class _PostStub:
96
+ def __init__(self) -> None:
97
+ self.called = False
98
+ self.body: dict | None = None
99
+
100
+ def _route_post_auth_start(self, body: dict) -> None:
101
+ self.called = True
102
+ self.body = body
103
+
104
+ _stub = _PostStub()
105
+ assert route_registry.dispatch_post(_stub, "/api/auth/start", {"email": "a@b.c"})
106
+ assert _stub.called and _stub.body == {"email": "a@b.c"}
107
+ assert not route_registry.dispatch_post(_stub, "/api/not-registered", {})
108
+
109
+ print("route registry self-check ok")
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """Granular bind + stream lease timings (docker exec)."""
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ import sys
7
+ import time
8
+ import uuid
9
+
10
+ import server
11
+
12
+
13
+ def ms(t0: float) -> int:
14
+ return round((time.perf_counter() - t0) * 1000)
15
+
16
+
17
+ def main() -> int:
18
+ conn = sqlite3.connect(str(server._workframe_db_path()))
19
+ conn.row_factory = sqlite3.Row
20
+ room = conn.execute(
21
+ """
22
+ SELECT id, workspace_id FROM rooms
23
+ WHERE room_type = 'direct' AND deleted_at IS NULL
24
+ ORDER BY updated_at DESC LIMIT 1
25
+ """,
26
+ ).fetchone()
27
+ user = conn.execute(
28
+ "SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
29
+ ).fetchone()
30
+ conn.close()
31
+ if not room or not user:
32
+ print("no room/user", file=sys.stderr)
33
+ return 1
34
+
35
+ uid = str(user["id"])
36
+ rid = str(room["id"])
37
+ ws = str(room["workspace_id"])
38
+ prof = server._resolve_chat_hermes_profile("mybusiness-agent", uid, rid, ws)
39
+ payload = {
40
+ "room_id": rid,
41
+ "workspace_id": ws,
42
+ "source_id": "steps",
43
+ "client_id": "steps",
44
+ "binding_version": 2,
45
+ }
46
+ print(f"profile={prof}")
47
+
48
+ block = server._read_model_block(prof)
49
+ t = time.perf_counter()
50
+ block2 = server._read_model_block(prof)
51
+ print(f"read_model_block_ms={ms(t)} repeat_ms={ms(t)}")
52
+
53
+ t = time.perf_counter()
54
+ server.ensure_profile_api(prof, uid, ws)
55
+ print(f"ensure_api_ms={ms(t)}")
56
+
57
+ provider = server._llm_billing_provider(prof, user_id=uid, workspace_id=ws, block=block)
58
+ run_a, run_b = "steps-a", "steps-b"
59
+ t = time.perf_counter()
60
+ server._apply_turn_credential_lease(prof, uid, ws, provider, run_a)
61
+ print(f"lease_cold_ms={ms(t)} provider={provider}")
62
+ t = time.perf_counter()
63
+ server._apply_turn_credential_lease(prof, uid, ws, provider, run_b)
64
+ print(f"lease_warm_ms={ms(t)}")
65
+
66
+ for i in range(3):
67
+ t = time.perf_counter()
68
+ server.profile_chat_bind("", payload, user_id=uid)
69
+ print(f"bind_{i}_ms={ms(t)}")
70
+
71
+ return 0
72
+
73
+
74
+ if __name__ == "__main__":
75
+ raise SystemExit(main())
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ """Measure model-save, gateway reload, bind, and reconcile latency (docker exec)."""
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ import sys
7
+ import time
8
+
9
+ import server
10
+
11
+
12
+ def ms(t0: float) -> int:
13
+ return round((time.perf_counter() - t0) * 1000)
14
+
15
+
16
+ def main() -> int:
17
+ conn = sqlite3.connect(str(server._workframe_db_path()))
18
+ conn.row_factory = sqlite3.Row
19
+ room = conn.execute(
20
+ """
21
+ SELECT r.id, r.workspace_id
22
+ FROM rooms r
23
+ WHERE r.room_type = 'direct' AND r.deleted_at IS NULL
24
+ ORDER BY r.updated_at DESC
25
+ LIMIT 1
26
+ """,
27
+ ).fetchone()
28
+ user = conn.execute(
29
+ "SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
30
+ ).fetchone()
31
+ conn.close()
32
+ if not room or not user:
33
+ print("no room/user", file=sys.stderr)
34
+ return 1
35
+
36
+ user_id = str(user["id"])
37
+ room_id = str(room["id"])
38
+ ws_id = str(room["workspace_id"])
39
+ prof = server._resolve_chat_hermes_profile("mybusiness-agent", user_id, room_id, ws_id)
40
+ print(f"profile={prof}")
41
+
42
+ t = time.perf_counter()
43
+ ok, err = server._apply_model_persisted(
44
+ prof, "codex", "gpt-5.4-mini", user_id, restart_gateway=False,
45
+ )
46
+ print(f"yaml_write_ms={ms(t)} ok={ok} err={err!r}")
47
+
48
+ t = time.perf_counter()
49
+ code, out = server._gateway_exec(prof, ["gateway", "restart"])
50
+ # ponytail: ~7s vs stop+start ~45s on dogfood
51
+ healthy = server._wait_profile_api_healthy(prof, attempts=24, delay=0.25)
52
+ print(f"gateway_restart_ms={ms(t)} code={code} healthy={healthy} out={out[:80]!r}")
53
+
54
+ t = time.perf_counter()
55
+ changed = server._reconcile_profile_llm_for_user(prof, user_id, ws_id)
56
+ print(f"reconcile_ms={ms(t)} changed={changed}")
57
+
58
+ t = time.perf_counter()
59
+ server.ensure_profile_api(prof, user_id, ws_id)
60
+ print(f"ensure_api_ms={ms(t)}")
61
+
62
+ payload = {
63
+ "room_id": room_id,
64
+ "workspace_id": ws_id,
65
+ "source_id": "perf",
66
+ "client_id": "perf",
67
+ "binding_version": 2,
68
+ }
69
+ t = time.perf_counter()
70
+ session = server.profile_chat_session("", payload, user_id=user_id)
71
+ print(f"bind_ms={ms(t)} session={str(session.get('session_id') or '')[:24]}")
72
+
73
+ t = time.perf_counter()
74
+ changed2 = server._reconcile_profile_llm_for_user(prof, user_id, ws_id)
75
+ print(f"reconcile_repeat_ms={ms(t)} changed={changed2}")
76
+
77
+ return 0
78
+
79
+
80
+ if __name__ == "__main__":
81
+ raise SystemExit(main())
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """Warm-path timings: model save API return + bind + first stream token."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ import sys
8
+ import time
9
+ import urllib.error
10
+ import urllib.request
11
+
12
+ import server
13
+
14
+
15
+ def ms(t0: float) -> int:
16
+ return round((time.perf_counter() - t0) * 1000)
17
+
18
+
19
+ def main() -> int:
20
+ conn = sqlite3.connect(str(server._workframe_db_path()))
21
+ conn.row_factory = sqlite3.Row
22
+ room = conn.execute(
23
+ """
24
+ SELECT id, workspace_id FROM rooms
25
+ WHERE room_type = 'direct' AND deleted_at IS NULL
26
+ ORDER BY updated_at DESC LIMIT 1
27
+ """,
28
+ ).fetchone()
29
+ user = conn.execute(
30
+ "SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
31
+ ).fetchone()
32
+ conn.close()
33
+ if not room or not user:
34
+ print("no room/user", file=sys.stderr)
35
+ return 1
36
+
37
+ uid = str(user["id"])
38
+ rid = str(room["id"])
39
+ ws = str(room["workspace_id"])
40
+ prof = server._resolve_chat_hermes_profile("mybusiness-agent", uid, rid, ws)
41
+ payload = {
42
+ "room_id": rid,
43
+ "workspace_id": ws,
44
+ "source_id": "warm",
45
+ "client_id": "warm",
46
+ "binding_version": 2,
47
+ }
48
+ print(f"profile={prof} healthy={server._profile_api_healthy(prof)}")
49
+
50
+ t = time.perf_counter()
51
+ result = server.hermes_model_set(
52
+ prof, "gpt-5.4-medium", user_id=uid, workspace_id=ws, billing_provider="codex",
53
+ )
54
+ print(f"model_set_ms={ms(t)} ok={result.get('ok')}")
55
+
56
+ time.sleep(0.5)
57
+ t = time.perf_counter()
58
+ session = server.profile_chat_session("", payload, user_id=uid)
59
+ sid = str(session.get("session_id") or "")
60
+ print(f"bind_ms={ms(t)} session={sid[:28]}")
61
+
62
+ t = time.perf_counter()
63
+ server.profile_chat_session("", payload, user_id=uid)
64
+ print(f"bind_repeat_ms={ms(t)}")
65
+
66
+ for i in range(2):
67
+ t = time.perf_counter()
68
+ server._reconcile_profile_llm_for_user(prof, uid, ws)
69
+ print(f"reconcile_cached_{i}_ms={ms(t)}")
70
+
71
+ t = time.perf_counter()
72
+ server.profile_chat_session("", payload, user_id=uid)
73
+ print(f"bind_cached_ms={ms(t)}")
74
+
75
+ port = server._profile_api_port(prof)
76
+ key = server._profile_api_key(prof)
77
+ body = json.dumps(server._profile_turn_payload(prof, "Reply with exactly: pong", rid)).encode()
78
+ url = f"http://gateway:{port}/api/sessions/{sid}/chat/stream"
79
+ req = urllib.request.Request(
80
+ url,
81
+ data=body,
82
+ headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
83
+ method="POST",
84
+ )
85
+ t = time.perf_counter()
86
+ first_ms = None
87
+ try:
88
+ with urllib.request.urlopen(req, timeout=120) as resp:
89
+ while True:
90
+ chunk = resp.read(4096)
91
+ if not chunk:
92
+ break
93
+ if first_ms is None and b"data:" in chunk:
94
+ first_ms = ms(t)
95
+ print(f"first_upstream_chunk_ms={first_ms} sample={chunk[:120]!r}")
96
+ break
97
+ except urllib.error.HTTPError as exc:
98
+ print(f"stream_http_error={exc.code} body={exc.read()[:200]!r}")
99
+ return 1
100
+ if first_ms is None:
101
+ print("no_stream_chunk")
102
+ return 1
103
+ return 0
104
+
105
+
106
+ if __name__ == "__main__":
107
+ raise SystemExit(main())
@@ -1 +1 @@
1
- import{n as e,t}from"./path-BWPyau1x.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-CPbiDkEZ.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i)if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
1
+ import{n as e,t}from"./path-BWPyau1x.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-Cz2turIT.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i)if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
@@ -0,0 +1 @@
1
+ import"./chunk-NNHCCRGN-DlpIbxXb.js";import{x as e}from"./mermaid-parser.core-C_jMeF0a.js";export{e as createArchitectureServices};