@trucore/openclaw-atf 0.1.1 → 0.2.0

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/src/doctor.mjs ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * doctor.mjs — Integration doctor / readiness check for ATF OpenClaw plugin
3
+ *
4
+ * Answers:
5
+ * - Is the plugin loaded under the canonical ID?
6
+ * - Is the ATF CLI available?
7
+ * - Is the ATF API reachable?
8
+ * - Which backend is preferred / effective?
9
+ * - Are expected native tools registered?
10
+ * - Are there configuration warnings?
11
+ * - What should the operator do next?
12
+ *
13
+ * Exports:
14
+ * - runDoctor(cfg, registeredToolNames) → DoctorReport
15
+ * - EXPECTED_TOOLS → canonical tool list
16
+ * - DOCTOR_STATUS → status enum
17
+ *
18
+ * Uses ONLY built-in Node modules + sibling backend.mjs. No external deps.
19
+ */
20
+
21
+ import {
22
+ resolveBackend,
23
+ probeCliAvailable,
24
+ probeApiAvailable,
25
+ BACKEND_STATUS,
26
+ } from "./backend.mjs";
27
+
28
+ import {
29
+ validateConfig,
30
+ CANONICAL_PLUGIN_ID,
31
+ } from "./config.mjs";
32
+
33
+ // Import constants at module level — avoids circular dependency since
34
+ // index.mjs imports doctor.mjs lazily (dynamic import inside handler).
35
+ // At call-time, index.mjs is already fully initialized.
36
+
37
+ // Canonical plugin identity — duplicated from index.mjs to avoid circular
38
+ // import at module level. Kept in sync by version consistency tests.
39
+ const PLUGIN_ID = "trucore-atf";
40
+ const PLUGIN_VERSION = "0.2.0";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Constants
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /** Canonical set of expected native tools in this plugin version. */
47
+ export const EXPECTED_TOOLS = Object.freeze([
48
+ "atf_health",
49
+ "atf_discover",
50
+ "atf_bootstrap_plan",
51
+ "atf_bootstrap_execute_safe",
52
+ "atf_protect_intent",
53
+ "atf_verify_receipt",
54
+ "atf_report_savings",
55
+ "atf_integration_doctor",
56
+ "atf_bot_preflight",
57
+ "atf_tx_explain",
58
+ "atf_billing_info",
59
+ "atf_adoption_advisor",
60
+ "atf_billing_claim",
61
+ ]);
62
+
63
+ /** Doctor overall status — stable finite set. */
64
+ export const DOCTOR_STATUS = Object.freeze({
65
+ OK: "ok",
66
+ DEGRADED: "degraded",
67
+ MISCONFIGURED: "misconfigured",
68
+ UNAVAILABLE: "unavailable",
69
+ });
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Doctor report
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * @typedef {object} DoctorReport
77
+ * @property {string} plugin_id
78
+ * @property {string} plugin_version
79
+ * @property {boolean} plugin_loaded
80
+ * @property {boolean} lifecycle_exports_present
81
+ * @property {boolean} cli_available
82
+ * @property {boolean} api_available
83
+ * @property {string} preferred_backend
84
+ * @property {string} effective_backend
85
+ * @property {boolean} fallback_occurred
86
+ * @property {string|null} fallback_reason
87
+ * @property {string[]} native_tools_expected
88
+ * @property {string[]} native_tools_available
89
+ * @property {string[]} native_tools_missing
90
+ * @property {string} status
91
+ * @property {string[]} warnings
92
+ * @property {string[]} remediation
93
+ * @property {object} config_validation
94
+ */
95
+
96
+ /**
97
+ * Run the integration doctor check.
98
+ *
99
+ * @param {Record<string, unknown>} cfg Resolved plugin config.
100
+ * @param {string[]} registeredToolNames Names of tools currently registered.
101
+ * @param {Record<string, unknown>} [rawUserConfig] Original user config (pre-resolve) for validation.
102
+ * @returns {Promise<DoctorReport>}
103
+ */
104
+ export async function runDoctor(cfg, registeredToolNames = [], rawUserConfig) {
105
+ // 1. Probe backends
106
+ const cli = await probeCliAvailable(cfg?.atfCli ?? "atf");
107
+ const api = await probeApiAvailable(cfg?.atfBaseUrl);
108
+ const backend = await resolveBackend(cfg, { cli, api });
109
+
110
+ // 2. Check tool registration
111
+ const registeredSet = new Set(registeredToolNames);
112
+ const toolsMissing = EXPECTED_TOOLS.filter((t) => !registeredSet.has(t));
113
+ const toolsAvailable = EXPECTED_TOOLS.filter((t) => registeredSet.has(t));
114
+
115
+ // 3. Lifecycle exports presence
116
+ const lifecyclePresent = true; // verified by ES import resolution
117
+
118
+ // 4. Config validation
119
+ const configResult = validateConfig(rawUserConfig ?? cfg);
120
+
121
+ // 5. Build warnings + remediation
122
+ /** @type {string[]} */
123
+ const warnings = [...backend.warnings];
124
+ /** @type {string[]} */
125
+ const remediation = [];
126
+
127
+ // Merge config validation warnings/errors into doctor output
128
+ if (!configResult.valid) {
129
+ for (const err of configResult.errors) {
130
+ warnings.push(`Config error: ${err}`);
131
+ }
132
+ for (const rem of configResult.remediation) {
133
+ remediation.push(rem);
134
+ }
135
+ }
136
+ for (const w of configResult.warnings) {
137
+ warnings.push(`Config warning: ${w}`);
138
+ }
139
+
140
+ if (!cli.available) {
141
+ remediation.push("Install ATF CLI: npm install -g @trucore/atf");
142
+ }
143
+ if (cfg?.prefer === "api" && !cfg?.atfBaseUrl) {
144
+ remediation.push(
145
+ "Set atfBaseUrl in plugin config (e.g. 'https://api.trucore.xyz').",
146
+ );
147
+ }
148
+ if (cfg?.prefer === "api" && cfg?.atfBaseUrl && !api.available) {
149
+ remediation.push(
150
+ `Check ATF API at ${cfg.atfBaseUrl}/health — currently unreachable.`,
151
+ );
152
+ }
153
+ if (toolsMissing.length > 0) {
154
+ warnings.push(
155
+ `Missing tools: ${toolsMissing.join(", ")}. ` +
156
+ "Ensure the plugin loaded without errors.",
157
+ );
158
+ remediation.push(
159
+ "Restart the OpenClaw gateway and check logs for [trucore-atf] warnings.",
160
+ );
161
+ }
162
+
163
+ // 6. Determine overall status
164
+ let status = DOCTOR_STATUS.OK;
165
+ if (!configResult.valid) {
166
+ status = DOCTOR_STATUS.MISCONFIGURED;
167
+ } else if (backend.status === BACKEND_STATUS.UNAVAILABLE) {
168
+ status = DOCTOR_STATUS.UNAVAILABLE;
169
+ } else if (backend.status === BACKEND_STATUS.MISCONFIGURED) {
170
+ status = DOCTOR_STATUS.MISCONFIGURED;
171
+ } else if (
172
+ backend.status === BACKEND_STATUS.DEGRADED ||
173
+ toolsMissing.length > 0
174
+ ) {
175
+ status = DOCTOR_STATUS.DEGRADED;
176
+ }
177
+
178
+ // 7. Add "what to do next" if healthy
179
+ if (status === DOCTOR_STATUS.OK && remediation.length === 0) {
180
+ remediation.push("Integration is healthy. No action required.");
181
+ }
182
+
183
+ return {
184
+ plugin_id: PLUGIN_ID,
185
+ plugin_version: PLUGIN_VERSION,
186
+ plugin_loaded: true,
187
+ lifecycle_exports_present: lifecyclePresent,
188
+ cli_available: cli.available,
189
+ api_available: api.available,
190
+ preferred_backend: backend.preferred_backend,
191
+ effective_backend: backend.effective_backend,
192
+ fallback_occurred: backend.fallback_occurred,
193
+ fallback_reason: backend.fallback_reason,
194
+ native_tools_expected: [...EXPECTED_TOOLS],
195
+ native_tools_available: toolsAvailable,
196
+ native_tools_missing: toolsMissing,
197
+ status,
198
+ warnings,
199
+ remediation,
200
+ config_validation: {
201
+ valid: configResult.valid,
202
+ errors: configResult.errors,
203
+ warnings: configResult.warnings,
204
+ unsupported_keys: configResult.unsupported_keys,
205
+ },
206
+ };
207
+ }