codebee 0.1.102 → 0.1.103

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 (121) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -21
  3. package/README.md +4 -8
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -100
  12. package/app/core/acp_client.py +457 -457
  13. package/app/core/aiflavor.py +197 -197
  14. package/app/core/analytics.py +38 -38
  15. package/app/core/artifacts.py +148 -148
  16. package/app/core/assets.py +110 -110
  17. package/app/core/attachments.py +623 -623
  18. package/app/core/audit.py +30 -30
  19. package/app/core/backend_profiles.py +188 -188
  20. package/app/core/backup.py +470 -470
  21. package/app/core/beekeeper.py +183 -183
  22. package/app/core/benchstore.py +102 -102
  23. package/app/core/bookmeta.py +1036 -1036
  24. package/app/core/bookmeta_catalog.py +180 -180
  25. package/app/core/branching.py +137 -137
  26. package/app/core/capability.py +144 -144
  27. package/app/core/chaptersafe.py +32 -32
  28. package/app/core/cleanup.py +303 -303
  29. package/app/core/command_auth.py +302 -302
  30. package/app/core/compaction.py +201 -201
  31. package/app/core/contracts.py +255 -255
  32. package/app/core/covergen.py +356 -356
  33. package/app/core/defectretro.py +43 -43
  34. package/app/core/diagnostics.py +114 -114
  35. package/app/core/dispatch.py +1 -1
  36. package/app/core/dispatch_log.py +128 -128
  37. package/app/core/env_scrub.py +85 -85
  38. package/app/core/errorlog.py +137 -137
  39. package/app/core/eval_matrix.py +91 -91
  40. package/app/core/evaluation.py +174 -174
  41. package/app/core/failure_notify.py +290 -290
  42. package/app/core/flow_graph.py +33 -33
  43. package/app/core/goal_service.py +167 -167
  44. package/app/core/health.py +389 -389
  45. package/app/core/history.py +38 -38
  46. package/app/core/hooks.py +162 -162
  47. package/app/core/knowledge_pipeline.py +86 -86
  48. package/app/core/manager.py +2385 -2385
  49. package/app/core/mocks.py +64 -64
  50. package/app/core/notify.py +201 -201
  51. package/app/core/novel_quality.py +181 -181
  52. package/app/core/openapi.py +217 -217
  53. package/app/core/operations.py +228 -228
  54. package/app/core/paihang.py +145 -145
  55. package/app/core/pipeline.py +2 -2
  56. package/app/core/portguard.py +111 -111
  57. package/app/core/presence.py +77 -77
  58. package/app/core/preview.py +293 -293
  59. package/app/core/project_memory.py +206 -206
  60. package/app/core/prompt_guard.py +78 -78
  61. package/app/core/publish/__init__.py +7 -7
  62. package/app/core/publish/auto.py +659 -659
  63. package/app/core/publish/chaprows.py +138 -138
  64. package/app/core/publish/flows-fanqie-calibrated.json +442 -442
  65. package/app/core/publish/manager.py +1185 -1185
  66. package/app/core/publish/qimao.py +193 -193
  67. package/app/core/publish/volumes.py +108 -108
  68. package/app/core/publish/ws.py +139 -139
  69. package/app/core/quality_gate.py +201 -201
  70. package/app/core/redact.py +38 -38
  71. package/app/core/repeat_guard.py +129 -129
  72. package/app/core/retrieval.py +137 -137
  73. package/app/core/sandbox_runtime.py +95 -95
  74. package/app/core/selfupdate.py +384 -384
  75. package/app/core/session_log.py +161 -161
  76. package/app/core/sessions.py +312 -312
  77. package/app/core/settings.py +215 -215
  78. package/app/core/settings_schema.py +282 -282
  79. package/app/core/share_page.py +114 -114
  80. package/app/core/skill_scan.py +139 -139
  81. package/app/core/skillpacks/market/character-bible.md +66 -66
  82. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  83. package/app/core/skillpacks/market/git-workflow.md +57 -57
  84. package/app/core/skillpacks/market/release-notes.md +72 -72
  85. package/app/core/skillpacks/market/weekly-report.md +71 -71
  86. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  87. package/app/core/step_runner.py +132 -132
  88. package/app/core/story_tracking.py +275 -275
  89. package/app/core/task_compile.py +199 -199
  90. package/app/core/telemetry.py +292 -292
  91. package/app/core/tlsctx.py +64 -64
  92. package/app/core/token_meter.py +147 -147
  93. package/app/core/tool_outputs.py +107 -107
  94. package/app/core/tracing.py +207 -207
  95. package/app/core/upstream.py +41 -41
  96. package/app/core/usage.py +1038 -1038
  97. package/app/core/volumes.py +389 -389
  98. package/app/core/webhooks.py +89 -89
  99. package/app/core/wxdigest.py +865 -865
  100. package/app/core/zentao.py +2692 -2692
  101. package/app/mcp_server.py +244 -244
  102. package/app/output/_createbook_body.json +1 -0
  103. package/app/output/_probe_fq_click.py +50 -0
  104. package/app/output/_probe_fq_form.py +42 -0
  105. package/app/output/_probe_fq_modal.py +55 -0
  106. package/app/output/_probe_fq_points.py +71 -0
  107. package/app/output/_probe_fq_submit.py +34 -0
  108. package/app/output/_probe_fq_watch.py +62 -0
  109. package/app/output/workbench-clean-preview.jpg +0 -0
  110. package/app/output/workbench-reference-render.png +0 -0
  111. package/app/output/workbench-shell.png +0 -0
  112. package/app/pick_dialog.py +110 -110
  113. package/app/ui/app.js +2 -3
  114. package/app/ui/artdiff.js +74 -74
  115. package/app/ui/i18n.js +0 -1
  116. package/app/ui/icons/bee.svg +79 -79
  117. package/app/ui/manifest.json +16 -16
  118. package/app/ui/qrcode.js +2297 -2297
  119. package/bin/tutti.js +147 -147
  120. package/package.json +1 -1
  121. package/tools/wx_reader.py +261 -261
@@ -1,174 +1,174 @@
1
- # -*- coding: utf-8 -*-
2
- """Expiring model/provider evaluation conclusions.
3
-
4
- An evaluation is evidence captured at a point in time, not a permanent
5
- capability claim. Results are kept as a small, content-free local ledger so
6
- the UI or a router can distinguish a fresh ``passed`` result from an expired
7
- one. A provider health failure invalidates matching conclusions immediately;
8
- the health probe can show recovery, but only a fresh evaluation restores
9
- ``passed``.
10
- """
11
- from __future__ import annotations
12
-
13
- import json
14
- import math
15
- import os
16
- from pathlib import Path
17
- import threading
18
- import time
19
-
20
- DEFAULT_TTL_SECONDS = 24 * 60 * 60
21
- _LOCK = threading.RLock()
22
-
23
-
24
- def _path() -> Path:
25
- from . import paths
26
- return Path(paths.DATA_DIR) / "evaluations.json"
27
-
28
-
29
- def ttl_seconds() -> float:
30
- try:
31
- return max(60.0, float(os.environ.get(
32
- "TUTTI_EVALUATION_TTL_S", DEFAULT_TTL_SECONDS)))
33
- except (TypeError, ValueError):
34
- return float(DEFAULT_TTL_SECONDS)
35
-
36
-
37
- def _read() -> dict:
38
- try:
39
- value = json.loads(_path().read_text(encoding="utf-8"))
40
- return value if isinstance(value, dict) else {}
41
- except (OSError, json.JSONDecodeError):
42
- return {}
43
-
44
-
45
- def _write(value: dict) -> None:
46
- path = _path()
47
- try:
48
- path.parent.mkdir(parents=True, exist_ok=True)
49
- tmp = path.with_suffix(".tmp")
50
- tmp.write_text(json.dumps(value, ensure_ascii=False, indent=2),
51
- encoding="utf-8")
52
- tmp.replace(path)
53
- except OSError:
54
- # Evaluation is telemetry. A read-only data directory must not turn
55
- # a successful model call into a task failure.
56
- pass
57
-
58
-
59
- def key(kind: str, provider_id: str, model: str = "", protocol: str = "",
60
- key_id: str = "") -> str:
61
- base = "%s|%s|%s" % (str(kind or "provider"),
62
- str(provider_id or ""), str(model or ""))
63
- suffix = "|".join((str(protocol or ""), str(key_id or "")))
64
- return base if not suffix.strip("|") else base + "|" + suffix
65
-
66
-
67
- def decorate(result: dict, *, now=None) -> dict:
68
- """Attach an evaluated-at timestamp and expiry to an API response."""
69
- out = dict(result or {})
70
- now = time.time() if now is None else float(now)
71
- out["evaluated_at"] = now
72
- out["expires_at"] = now + ttl_seconds()
73
- out["fresh"] = True
74
- return out
75
-
76
-
77
- def record(kind: str, provider_id: str, model: str, result: dict,
78
- *, protocol: str = "", requested_model: str = "",
79
- served_model: str = "", key_id: str = "") -> dict:
80
- """Persist safe metadata for one completed evaluation and return it."""
81
- out = decorate(result)
82
- entry = {
83
- "kind": str(kind or "provider"),
84
- "provider_id": str(provider_id or ""),
85
- "model": str(model or ""),
86
- "status": str(out.get("status") or "failed"),
87
- "ok": bool(out.get("ok")),
88
- "evaluated_at": out["evaluated_at"],
89
- "expires_at": out["expires_at"],
90
- "protocol": str(protocol or ""),
91
- "key_id": str(key_id or result.get("key_id") or ""),
92
- "requested_model": str(requested_model or model or ""),
93
- "served_model": str(served_model or ""),
94
- }
95
- with _LOCK:
96
- data = _read()
97
- data.setdefault("entries", {})[key(
98
- kind, provider_id, model, protocol, key_id or result.get("key_id") or "")] = entry
99
- _write(data)
100
- return out
101
-
102
-
103
- def invalidate_provider(provider_id: str, model: str = "",
104
- *, reason: str = "health_failure") -> None:
105
- """Invalidate one model, or all conclusions, after a health event."""
106
- provider_id = str(provider_id or "").strip()
107
- if not provider_id:
108
- return
109
- with _LOCK:
110
- data = _read()
111
- at = time.time()
112
- invalidations = data.setdefault("invalidated", {})
113
- invalidations[provider_id] = {
114
- "at": at, "reason": str(reason or "health_failure")[:80]}
115
- if model:
116
- invalidations[key("model", provider_id, model)] = {
117
- "at": at, "reason": str(reason or "health_failure")[:80]}
118
- _write(data)
119
-
120
-
121
- def _safe_float(value, default=0.0) -> float:
122
- try:
123
- result = float(value)
124
- return result if math.isfinite(result) else float(default)
125
- except (TypeError, ValueError):
126
- return float(default)
127
-
128
-
129
- def get(kind: str, provider_id: str, model: str = "", *, now=None):
130
- """Read an evaluation, returning a stale marker after TTL/health expiry."""
131
- now = time.time() if now is None else float(now)
132
- with _LOCK:
133
- data = _read()
134
- entries = data.get("entries") or {}
135
- base_key = key(kind, provider_id, model)
136
- entry = entries.get(base_key)
137
- matches = [value for name, value in entries.items()
138
- if str(name).startswith(base_key + "|") and isinstance(value, dict)]
139
- candidates = ([entry] if isinstance(entry, dict) else []) + matches
140
- entry = max(candidates, key=lambda value: _safe_float(value.get("evaluated_at")),
141
- default=None)
142
- if not isinstance(entry, dict):
143
- return None
144
- out = dict(entry)
145
- invalidated = data.get("invalidated") or {}
146
- exact_invalid = invalidated.get(key(kind, provider_id, model)) or {}
147
- provider_invalid = invalidated.get(str(provider_id or "")) or {}
148
- invalid = max((candidate for candidate in (exact_invalid, provider_invalid)
149
- if isinstance(candidate, dict)),
150
- key=lambda candidate: _safe_float(candidate.get("at")),
151
- default={})
152
- invalid_at = _safe_float(invalid.get("at"))
153
- expires_at = _safe_float(out.get("expires_at"))
154
- stale_reason = ""
155
- if invalid_at and invalid_at >= _safe_float(out.get("evaluated_at")):
156
- stale_reason = str(invalid.get("reason") or "health_failure")
157
- elif expires_at <= now:
158
- stale_reason = "ttl_expired"
159
- if stale_reason:
160
- out["fresh"] = False
161
- out["ok"] = False
162
- out["status"] = "stale"
163
- out["stale_reason"] = stale_reason
164
- else:
165
- out["fresh"] = True
166
- return out
167
-
168
-
169
- def reset_for_tests() -> None:
170
- with _LOCK:
171
- try:
172
- _path().unlink()
173
- except OSError:
174
- pass
1
+ # -*- coding: utf-8 -*-
2
+ """Expiring model/provider evaluation conclusions.
3
+
4
+ An evaluation is evidence captured at a point in time, not a permanent
5
+ capability claim. Results are kept as a small, content-free local ledger so
6
+ the UI or a router can distinguish a fresh ``passed`` result from an expired
7
+ one. A provider health failure invalidates matching conclusions immediately;
8
+ the health probe can show recovery, but only a fresh evaluation restores
9
+ ``passed``.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import math
15
+ import os
16
+ from pathlib import Path
17
+ import threading
18
+ import time
19
+
20
+ DEFAULT_TTL_SECONDS = 24 * 60 * 60
21
+ _LOCK = threading.RLock()
22
+
23
+
24
+ def _path() -> Path:
25
+ from . import paths
26
+ return Path(paths.DATA_DIR) / "evaluations.json"
27
+
28
+
29
+ def ttl_seconds() -> float:
30
+ try:
31
+ return max(60.0, float(os.environ.get(
32
+ "TUTTI_EVALUATION_TTL_S", DEFAULT_TTL_SECONDS)))
33
+ except (TypeError, ValueError):
34
+ return float(DEFAULT_TTL_SECONDS)
35
+
36
+
37
+ def _read() -> dict:
38
+ try:
39
+ value = json.loads(_path().read_text(encoding="utf-8"))
40
+ return value if isinstance(value, dict) else {}
41
+ except (OSError, json.JSONDecodeError):
42
+ return {}
43
+
44
+
45
+ def _write(value: dict) -> None:
46
+ path = _path()
47
+ try:
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+ tmp = path.with_suffix(".tmp")
50
+ tmp.write_text(json.dumps(value, ensure_ascii=False, indent=2),
51
+ encoding="utf-8")
52
+ tmp.replace(path)
53
+ except OSError:
54
+ # Evaluation is telemetry. A read-only data directory must not turn
55
+ # a successful model call into a task failure.
56
+ pass
57
+
58
+
59
+ def key(kind: str, provider_id: str, model: str = "", protocol: str = "",
60
+ key_id: str = "") -> str:
61
+ base = "%s|%s|%s" % (str(kind or "provider"),
62
+ str(provider_id or ""), str(model or ""))
63
+ suffix = "|".join((str(protocol or ""), str(key_id or "")))
64
+ return base if not suffix.strip("|") else base + "|" + suffix
65
+
66
+
67
+ def decorate(result: dict, *, now=None) -> dict:
68
+ """Attach an evaluated-at timestamp and expiry to an API response."""
69
+ out = dict(result or {})
70
+ now = time.time() if now is None else float(now)
71
+ out["evaluated_at"] = now
72
+ out["expires_at"] = now + ttl_seconds()
73
+ out["fresh"] = True
74
+ return out
75
+
76
+
77
+ def record(kind: str, provider_id: str, model: str, result: dict,
78
+ *, protocol: str = "", requested_model: str = "",
79
+ served_model: str = "", key_id: str = "") -> dict:
80
+ """Persist safe metadata for one completed evaluation and return it."""
81
+ out = decorate(result)
82
+ entry = {
83
+ "kind": str(kind or "provider"),
84
+ "provider_id": str(provider_id or ""),
85
+ "model": str(model or ""),
86
+ "status": str(out.get("status") or "failed"),
87
+ "ok": bool(out.get("ok")),
88
+ "evaluated_at": out["evaluated_at"],
89
+ "expires_at": out["expires_at"],
90
+ "protocol": str(protocol or ""),
91
+ "key_id": str(key_id or result.get("key_id") or ""),
92
+ "requested_model": str(requested_model or model or ""),
93
+ "served_model": str(served_model or ""),
94
+ }
95
+ with _LOCK:
96
+ data = _read()
97
+ data.setdefault("entries", {})[key(
98
+ kind, provider_id, model, protocol, key_id or result.get("key_id") or "")] = entry
99
+ _write(data)
100
+ return out
101
+
102
+
103
+ def invalidate_provider(provider_id: str, model: str = "",
104
+ *, reason: str = "health_failure") -> None:
105
+ """Invalidate one model, or all conclusions, after a health event."""
106
+ provider_id = str(provider_id or "").strip()
107
+ if not provider_id:
108
+ return
109
+ with _LOCK:
110
+ data = _read()
111
+ at = time.time()
112
+ invalidations = data.setdefault("invalidated", {})
113
+ invalidations[provider_id] = {
114
+ "at": at, "reason": str(reason or "health_failure")[:80]}
115
+ if model:
116
+ invalidations[key("model", provider_id, model)] = {
117
+ "at": at, "reason": str(reason or "health_failure")[:80]}
118
+ _write(data)
119
+
120
+
121
+ def _safe_float(value, default=0.0) -> float:
122
+ try:
123
+ result = float(value)
124
+ return result if math.isfinite(result) else float(default)
125
+ except (TypeError, ValueError):
126
+ return float(default)
127
+
128
+
129
+ def get(kind: str, provider_id: str, model: str = "", *, now=None):
130
+ """Read an evaluation, returning a stale marker after TTL/health expiry."""
131
+ now = time.time() if now is None else float(now)
132
+ with _LOCK:
133
+ data = _read()
134
+ entries = data.get("entries") or {}
135
+ base_key = key(kind, provider_id, model)
136
+ entry = entries.get(base_key)
137
+ matches = [value for name, value in entries.items()
138
+ if str(name).startswith(base_key + "|") and isinstance(value, dict)]
139
+ candidates = ([entry] if isinstance(entry, dict) else []) + matches
140
+ entry = max(candidates, key=lambda value: _safe_float(value.get("evaluated_at")),
141
+ default=None)
142
+ if not isinstance(entry, dict):
143
+ return None
144
+ out = dict(entry)
145
+ invalidated = data.get("invalidated") or {}
146
+ exact_invalid = invalidated.get(key(kind, provider_id, model)) or {}
147
+ provider_invalid = invalidated.get(str(provider_id or "")) or {}
148
+ invalid = max((candidate for candidate in (exact_invalid, provider_invalid)
149
+ if isinstance(candidate, dict)),
150
+ key=lambda candidate: _safe_float(candidate.get("at")),
151
+ default={})
152
+ invalid_at = _safe_float(invalid.get("at"))
153
+ expires_at = _safe_float(out.get("expires_at"))
154
+ stale_reason = ""
155
+ if invalid_at and invalid_at >= _safe_float(out.get("evaluated_at")):
156
+ stale_reason = str(invalid.get("reason") or "health_failure")
157
+ elif expires_at <= now:
158
+ stale_reason = "ttl_expired"
159
+ if stale_reason:
160
+ out["fresh"] = False
161
+ out["ok"] = False
162
+ out["status"] = "stale"
163
+ out["stale_reason"] = stale_reason
164
+ else:
165
+ out["fresh"] = True
166
+ return out
167
+
168
+
169
+ def reset_for_tests() -> None:
170
+ with _LOCK:
171
+ try:
172
+ _path().unlink()
173
+ except OSError:
174
+ pass