@genn-inc/cluebase-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/bin/cluebase-cli.mjs +11 -0
- package/package.json +17 -0
- package/src/cli-command.mjs +515 -0
- package/src/cli-invocation.mjs +17 -0
- package/src/code-evidence-analyzer.mjs +2041 -0
- package/src/contracts.mjs +36 -0
- package/src/generated-code-evidence-contract.mjs +22 -0
- package/src/generated-sdk-version-contract.mjs +5 -0
- package/src/generated-source-path-policy.mjs +20 -0
- package/src/lifecycle-guard.mjs +202 -0
- package/src/path-policy.mjs +81 -0
- package/src/setup-ai-contract.mjs +221 -0
- package/src/setup-check-constants.mjs +110 -0
- package/src/setup-check-scan-a.mjs +849 -0
- package/src/setup-check-scan-b.mjs +994 -0
- package/src/setup-check.mjs +575 -0
- package/src/setup-discover-check.mjs +755 -0
- package/src/setup-doctor-deadline.mjs +221 -0
- package/src/setup-doctor-env.mjs +331 -0
- package/src/setup-doctor-file-boundary.mjs +426 -0
- package/src/setup-doctor-probe.mjs +719 -0
- package/src/setup-doctor-quality-checks-a.mjs +593 -0
- package/src/setup-doctor-quality-checks-b.mjs +638 -0
- package/src/setup-doctor-quality-shared.mjs +382 -0
- package/src/setup-doctor-quality.mjs +209 -0
- package/src/setup-doctor-route-scan.mjs +160 -0
- package/src/setup-doctor-sdk-probe.mjs +340 -0
- package/src/setup-doctor.mjs +545 -0
- package/src/setup-documents.mjs +112 -0
- package/src/setup-help.mjs +130 -0
- package/src/setup-prepare.mjs +360 -0
- package/src/setup-repository-discovery.mjs +764 -0
- package/src/setup-step-builders-discover.mjs +701 -0
- package/src/setup-step-builders-events.mjs +229 -0
- package/src/setup-step-builders-implement.mjs +710 -0
- package/src/setup-step-commands.mjs +427 -0
- package/src/setup-tool.mjs +27 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
// setup-doctor: local API connectivity + data-quality preflight for Cluebase setup.
|
|
2
|
+
// Env/probe/route-scan helpers live in setup-doctor-env / -probe / -route-scan.
|
|
3
|
+
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { deriveEnvironmentFromProjectKey } from "./contracts.mjs";
|
|
6
|
+
import { API_CONNECTIVITY_CONTRACT } from "./setup-ai-contract.mjs";
|
|
7
|
+
import { loadQualityInputs, runQualityChecks } from "./setup-doctor-quality.mjs";
|
|
8
|
+
import {
|
|
9
|
+
throwIfSetupDoctorDeadlineExceeded,
|
|
10
|
+
withSetupDoctorDeadline,
|
|
11
|
+
} from "./setup-doctor-deadline.mjs";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_SETUP_MANIFEST_PATH,
|
|
14
|
+
CLUEBASE_BROWSER_TOKEN_PATH,
|
|
15
|
+
BROWSER_INGEST_PATH,
|
|
16
|
+
BACKEND_INGEST_PATH,
|
|
17
|
+
CLUEBASE_TEST_SETUP_SDK_VERSION,
|
|
18
|
+
optionalString,
|
|
19
|
+
readBatchVisibilityTimeoutConfig,
|
|
20
|
+
joinUrl,
|
|
21
|
+
BATCH_STATUS_PLACEHOLDER_PATH,
|
|
22
|
+
publicCluebaseApiBaseUrl,
|
|
23
|
+
publicProjectKeyFromEnv,
|
|
24
|
+
cluebaseApiBaseUrlFromIngestEndpoint,
|
|
25
|
+
normalizeBrowserIngestUrl,
|
|
26
|
+
firstTargetUrl,
|
|
27
|
+
loadDoctorEnv,
|
|
28
|
+
} from "./setup-doctor-env.mjs";
|
|
29
|
+
import { readSetupDoctorJson } from "./setup-doctor-file-boundary.mjs";
|
|
30
|
+
import {
|
|
31
|
+
firstTargetBackendTargetId,
|
|
32
|
+
postJson,
|
|
33
|
+
tokenFromResult,
|
|
34
|
+
observationBatchIdFromResult,
|
|
35
|
+
buildCheck,
|
|
36
|
+
pollBatchVisibility,
|
|
37
|
+
buildSetupCorrelation,
|
|
38
|
+
buildBrowserEventPayload,
|
|
39
|
+
buildBackendEventPayload,
|
|
40
|
+
} from "./setup-doctor-probe.mjs";
|
|
41
|
+
import {
|
|
42
|
+
requiredInputCheck,
|
|
43
|
+
collectCustomerBackendCluebaseRouteFiles,
|
|
44
|
+
resolveBackendRootCandidates,
|
|
45
|
+
} from "./setup-doctor-route-scan.mjs";
|
|
46
|
+
import {
|
|
47
|
+
manifestHasBackend,
|
|
48
|
+
manifestHasNodeBackend,
|
|
49
|
+
manifestHasPythonBackend,
|
|
50
|
+
nodeBackendPreloadCheck,
|
|
51
|
+
probeBackendSdkImport,
|
|
52
|
+
probeNodeBackendPreload,
|
|
53
|
+
backendSdkImportCheck,
|
|
54
|
+
} from "./setup-doctor-sdk-probe.mjs";
|
|
55
|
+
|
|
56
|
+
// A frontend-only setup (SPA + static/serverless hosting, no backend) has no
|
|
57
|
+
// backend ingest hop and no customer backend that could expose a Cluebase proxy
|
|
58
|
+
// route. FIX-116: mark those backend-specific checks as N/A-pass instead of
|
|
59
|
+
// failing them on absent backend inputs, which would otherwise make a valid
|
|
60
|
+
// frontend-only setup impossible to pass.
|
|
61
|
+
const frontendOnlyBackendNaCheck = (id) => ({
|
|
62
|
+
id,
|
|
63
|
+
severity: "error",
|
|
64
|
+
passed: true,
|
|
65
|
+
not_applicable: true,
|
|
66
|
+
skipped_reason:
|
|
67
|
+
"frontend-only setup (no backend detected); this backend check does not apply.",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const runSetupDoctor = withSetupDoctorDeadline(async ({
|
|
71
|
+
fetchImpl = fetch,
|
|
72
|
+
flags,
|
|
73
|
+
repoRoot = ".",
|
|
74
|
+
runPython,
|
|
75
|
+
}) => {
|
|
76
|
+
const manifestPath = String(
|
|
77
|
+
flags.get("manifest") || DEFAULT_SETUP_MANIFEST_PATH,
|
|
78
|
+
);
|
|
79
|
+
const resolvedRepoRoot = resolve(repoRoot);
|
|
80
|
+
const manifest = await readSetupDoctorJson({
|
|
81
|
+
repoRoot: resolvedRepoRoot,
|
|
82
|
+
path: manifestPath,
|
|
83
|
+
signal: fetchImpl.signal,
|
|
84
|
+
optional: true,
|
|
85
|
+
});
|
|
86
|
+
const doctorEnv = await loadDoctorEnv({
|
|
87
|
+
manifest,
|
|
88
|
+
repoRoot: resolvedRepoRoot,
|
|
89
|
+
signal: fetchImpl.signal,
|
|
90
|
+
});
|
|
91
|
+
const frontendEnv = doctorEnv.frontendEnv;
|
|
92
|
+
const backendEnv = doctorEnv.backendEnv;
|
|
93
|
+
const flagCluebaseApiBaseUrl = optionalString(flags.get("cluebase-api-base-url"));
|
|
94
|
+
const frontendCluebaseApiBaseUrl =
|
|
95
|
+
flagCluebaseApiBaseUrl ?? publicCluebaseApiBaseUrl(frontendEnv);
|
|
96
|
+
const backendCluebaseApiBaseUrl =
|
|
97
|
+
flagCluebaseApiBaseUrl ??
|
|
98
|
+
optionalString(backendEnv.CLUEBASE_API_BASE_URL) ??
|
|
99
|
+
cluebaseApiBaseUrlFromIngestEndpoint(backendEnv.CLUEBASE_INGEST_ENDPOINT);
|
|
100
|
+
const cluebaseApiBaseUrl = backendCluebaseApiBaseUrl ?? frontendCluebaseApiBaseUrl;
|
|
101
|
+
const flagProjectKey = optionalString(flags.get("project-key"));
|
|
102
|
+
const frontendProjectKey =
|
|
103
|
+
flagProjectKey ?? publicProjectKeyFromEnv(frontendEnv);
|
|
104
|
+
const backendProjectKey =
|
|
105
|
+
flagProjectKey ?? optionalString(backendEnv.CLUEBASE_PROJECT_KEY);
|
|
106
|
+
const projectKey = frontendProjectKey ?? backendProjectKey;
|
|
107
|
+
const environment = projectKey
|
|
108
|
+
? deriveEnvironmentFromProjectKey(projectKey)
|
|
109
|
+
: null;
|
|
110
|
+
const apiKey =
|
|
111
|
+
optionalString(flags.get("cluebase-api-key")) ??
|
|
112
|
+
optionalString(backendEnv.CLUEBASE_API_KEY);
|
|
113
|
+
const backendServiceKey =
|
|
114
|
+
optionalString(flags.get("backend-service-key")) ??
|
|
115
|
+
optionalString(manifest?.detected?.service_key) ??
|
|
116
|
+
firstTargetBackendTargetId(manifest);
|
|
117
|
+
const clientFrontendUrl =
|
|
118
|
+
optionalString(flags.get("client-frontend-url")) ??
|
|
119
|
+
firstTargetUrl({ manifest, kind: "frontend" });
|
|
120
|
+
const origin = optionalString(flags.get("origin")) ?? clientFrontendUrl;
|
|
121
|
+
const cluebaseBrowserTokenUrl = frontendCluebaseApiBaseUrl
|
|
122
|
+
? joinUrl(frontendCluebaseApiBaseUrl, CLUEBASE_BROWSER_TOKEN_PATH)
|
|
123
|
+
: null;
|
|
124
|
+
const browserIngestUrl = normalizeBrowserIngestUrl(
|
|
125
|
+
optionalString(flags.get("browser-ingest-url")) ??
|
|
126
|
+
(flagCluebaseApiBaseUrl
|
|
127
|
+
? joinUrl(flagCluebaseApiBaseUrl, BROWSER_INGEST_PATH)
|
|
128
|
+
: null) ??
|
|
129
|
+
publicCluebaseApiBaseUrl(frontendEnv) ??
|
|
130
|
+
(frontendCluebaseApiBaseUrl
|
|
131
|
+
? joinUrl(frontendCluebaseApiBaseUrl, BROWSER_INGEST_PATH)
|
|
132
|
+
: null),
|
|
133
|
+
);
|
|
134
|
+
const backendIngestUrl =
|
|
135
|
+
optionalString(flags.get("backend-ingest-url")) ??
|
|
136
|
+
(flagCluebaseApiBaseUrl
|
|
137
|
+
? joinUrl(flagCluebaseApiBaseUrl, BACKEND_INGEST_PATH)
|
|
138
|
+
: null) ??
|
|
139
|
+
optionalString(backendEnv.CLUEBASE_INGEST_ENDPOINT) ??
|
|
140
|
+
(backendCluebaseApiBaseUrl
|
|
141
|
+
? joinUrl(backendCluebaseApiBaseUrl, BACKEND_INGEST_PATH)
|
|
142
|
+
: null);
|
|
143
|
+
|
|
144
|
+
const checks = [fetchImpl.deadlineCheck].filter(Boolean);
|
|
145
|
+
// Real backend SDK import probe: catches published cluebase-backend-sdk versions
|
|
146
|
+
// that predate the `cluebase` facade (grep-based checks miss this and pass
|
|
147
|
+
// falsely while the customer backend crashes at import).
|
|
148
|
+
if (manifestHasPythonBackend(manifest)) {
|
|
149
|
+
const sdkProbe = await probeBackendSdkImport({
|
|
150
|
+
repoRoot: resolvedRepoRoot,
|
|
151
|
+
backendRootPath: optionalString(manifest?.detected?.backend_root_path),
|
|
152
|
+
...(runPython ? { runPython } : {}),
|
|
153
|
+
signal: fetchImpl.signal,
|
|
154
|
+
});
|
|
155
|
+
checks.push(backendSdkImportCheck(sdkProbe));
|
|
156
|
+
}
|
|
157
|
+
// Node backends must preload the SDK instrumentation entry. Without it the
|
|
158
|
+
// server starts, reports no error, and records nothing on the backend side —
|
|
159
|
+
// a failure no source-text check can see.
|
|
160
|
+
if (manifestHasNodeBackend(manifest)) {
|
|
161
|
+
const preloadProbe = await probeNodeBackendPreload({
|
|
162
|
+
repoRoot: resolvedRepoRoot,
|
|
163
|
+
backendRootPath: optionalString(manifest?.detected?.backend_root_path),
|
|
164
|
+
nodeOptions: optionalString(backendEnv.NODE_OPTIONS),
|
|
165
|
+
signal: fetchImpl.signal,
|
|
166
|
+
});
|
|
167
|
+
checks.push(nodeBackendPreloadCheck(preloadProbe));
|
|
168
|
+
}
|
|
169
|
+
// Backend hops apply when the manifest detected a backend, or the caller
|
|
170
|
+
// explicitly targets a backend via flags. Otherwise this is a frontend-only
|
|
171
|
+
// setup and the backend checks are N/A (FIX-116).
|
|
172
|
+
const explicitBackendTargetFlag =
|
|
173
|
+
flags.has("backend-ingest-url") ||
|
|
174
|
+
flags.has("backend-service-key") ||
|
|
175
|
+
flags.has("backend-root-path");
|
|
176
|
+
const backendApplicable =
|
|
177
|
+
manifestHasBackend(manifest) || explicitBackendTargetFlag;
|
|
178
|
+
const batchVisibilityTimeoutConfig = readBatchVisibilityTimeoutConfig();
|
|
179
|
+
if (batchVisibilityTimeoutConfig.error) {
|
|
180
|
+
checks.push({
|
|
181
|
+
id: "setup_timeout_config",
|
|
182
|
+
name: "setup-doctor batch visibility timeout configuration",
|
|
183
|
+
severity: "error",
|
|
184
|
+
passed: false,
|
|
185
|
+
error: batchVisibilityTimeoutConfig.error,
|
|
186
|
+
env_var: "CLUEBASE_SETUP_VERIFICATION_BATCH_TIMEOUT_MS",
|
|
187
|
+
timeout_ms: batchVisibilityTimeoutConfig.timeoutMs,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
let browserIngestBatchId = null;
|
|
191
|
+
const setupCorrelation = buildSetupCorrelation();
|
|
192
|
+
|
|
193
|
+
// Frontend SDK fetches its token from the Cluebase backend directly.
|
|
194
|
+
let browserToken = null;
|
|
195
|
+
if (!cluebaseBrowserTokenUrl || !frontendProjectKey || !origin) {
|
|
196
|
+
checks.push(
|
|
197
|
+
requiredInputCheck({
|
|
198
|
+
id: "cluebase_backend_browser_token_issue",
|
|
199
|
+
missing: [
|
|
200
|
+
...(!cluebaseBrowserTokenUrl ? ["cluebase-api-base-url"] : []),
|
|
201
|
+
...(!frontendProjectKey ? ["frontend public project key"] : []),
|
|
202
|
+
...(!origin ? ["client-frontend-url or origin"] : []),
|
|
203
|
+
],
|
|
204
|
+
url: cluebaseBrowserTokenUrl,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
const result = await postJson({
|
|
209
|
+
fetchImpl,
|
|
210
|
+
url: cluebaseBrowserTokenUrl,
|
|
211
|
+
headers: { origin },
|
|
212
|
+
body: {
|
|
213
|
+
projectKey: frontendProjectKey,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
browserToken = tokenFromResult(result);
|
|
217
|
+
checks.push(
|
|
218
|
+
buildCheck({
|
|
219
|
+
id: "cluebase_backend_browser_token_issue",
|
|
220
|
+
passed: Boolean(browserToken),
|
|
221
|
+
result,
|
|
222
|
+
url: cluebaseBrowserTokenUrl,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Frontend SDK sends browser events to Cluebase ingest with the token issued above.
|
|
228
|
+
if (!browserIngestUrl || !frontendProjectKey || !browserToken) {
|
|
229
|
+
checks.push(
|
|
230
|
+
requiredInputCheck({
|
|
231
|
+
id: "browser_ingest",
|
|
232
|
+
missing: [
|
|
233
|
+
...(!browserIngestUrl ? ["browser-ingest-url"] : []),
|
|
234
|
+
...(!frontendProjectKey ? ["frontend public project key"] : []),
|
|
235
|
+
...(!browserToken ? ["browser token"] : []),
|
|
236
|
+
],
|
|
237
|
+
url: browserIngestUrl,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
} else {
|
|
241
|
+
const result = await postJson({
|
|
242
|
+
fetchImpl,
|
|
243
|
+
url: browserIngestUrl,
|
|
244
|
+
headers: {
|
|
245
|
+
origin,
|
|
246
|
+
"x-cluebase-project-key": frontendProjectKey,
|
|
247
|
+
"x-cluebase-browser-token": browserToken,
|
|
248
|
+
"x-cluebase-sdk-request": "browser",
|
|
249
|
+
"x-cluebase-sdk-version": CLUEBASE_TEST_SETUP_SDK_VERSION,
|
|
250
|
+
"x-cluebase-source-schema-version": "1",
|
|
251
|
+
},
|
|
252
|
+
body: buildBrowserEventPayload({
|
|
253
|
+
correlation: setupCorrelation,
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
browserIngestBatchId = observationBatchIdFromResult(result);
|
|
257
|
+
checks.push(
|
|
258
|
+
buildCheck({
|
|
259
|
+
id: "browser_ingest",
|
|
260
|
+
passed: Boolean(
|
|
261
|
+
result.transportOk && result.response.ok && browserIngestBatchId,
|
|
262
|
+
),
|
|
263
|
+
error: browserIngestBatchId
|
|
264
|
+
? null
|
|
265
|
+
: "browser ingest response must include observation_ingest_batch_id",
|
|
266
|
+
result,
|
|
267
|
+
url: browserIngestUrl,
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (
|
|
273
|
+
!browserIngestBatchId ||
|
|
274
|
+
!cluebaseApiBaseUrl ||
|
|
275
|
+
!apiKey ||
|
|
276
|
+
!backendProjectKey
|
|
277
|
+
) {
|
|
278
|
+
checks.push(
|
|
279
|
+
requiredInputCheck({
|
|
280
|
+
id: "browser_ingest_reached_ledger",
|
|
281
|
+
missing: [
|
|
282
|
+
...(!browserIngestBatchId
|
|
283
|
+
? ["browser_observation_ingest_batch_id"]
|
|
284
|
+
: []),
|
|
285
|
+
...(!cluebaseApiBaseUrl ? ["api-base-url"] : []),
|
|
286
|
+
...(!apiKey ? ["CLUEBASE_API_KEY"] : []),
|
|
287
|
+
...(!backendProjectKey ? ["CLUEBASE_PROJECT_KEY"] : []),
|
|
288
|
+
],
|
|
289
|
+
url: cluebaseApiBaseUrl
|
|
290
|
+
? joinUrl(cluebaseApiBaseUrl, BATCH_STATUS_PLACEHOLDER_PATH)
|
|
291
|
+
: null,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
} else {
|
|
295
|
+
checks.push(
|
|
296
|
+
await pollBatchVisibility({
|
|
297
|
+
apiKey,
|
|
298
|
+
batchId: browserIngestBatchId,
|
|
299
|
+
checkId: "browser_ingest_reached_ledger",
|
|
300
|
+
cluebaseApiBaseUrl,
|
|
301
|
+
fetchImpl,
|
|
302
|
+
projectKey: backendProjectKey,
|
|
303
|
+
timeoutMs: batchVisibilityTimeoutConfig.timeoutMs,
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
// Backend SDK sends backend events to Cluebase ingest. Frontend-only setups have
|
|
308
|
+
// no backend, so these hops are N/A (FIX-116).
|
|
309
|
+
if (!backendApplicable) {
|
|
310
|
+
checks.push(frontendOnlyBackendNaCheck("backend_ingest"));
|
|
311
|
+
checks.push(frontendOnlyBackendNaCheck("backend_ingest_reached_ledger"));
|
|
312
|
+
} else {
|
|
313
|
+
let backendIngestBatchId = null;
|
|
314
|
+
if (
|
|
315
|
+
!backendIngestUrl ||
|
|
316
|
+
!apiKey ||
|
|
317
|
+
!backendProjectKey ||
|
|
318
|
+
!backendServiceKey
|
|
319
|
+
) {
|
|
320
|
+
checks.push(
|
|
321
|
+
requiredInputCheck({
|
|
322
|
+
id: "backend_ingest",
|
|
323
|
+
missing: [
|
|
324
|
+
...(!backendIngestUrl ? ["backend-ingest-url"] : []),
|
|
325
|
+
...(!apiKey ? ["CLUEBASE_API_KEY"] : []),
|
|
326
|
+
...(!backendProjectKey ? ["CLUEBASE_PROJECT_KEY"] : []),
|
|
327
|
+
...(!backendServiceKey ? ["backend-service-key"] : []),
|
|
328
|
+
],
|
|
329
|
+
url: backendIngestUrl,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
} else {
|
|
333
|
+
const result = await postJson({
|
|
334
|
+
fetchImpl,
|
|
335
|
+
url: backendIngestUrl,
|
|
336
|
+
headers: {
|
|
337
|
+
"x-cluebase-project-key": backendProjectKey,
|
|
338
|
+
"x-cluebase-api-key": apiKey,
|
|
339
|
+
},
|
|
340
|
+
body: buildBackendEventPayload({
|
|
341
|
+
backendServiceKey,
|
|
342
|
+
correlation: setupCorrelation,
|
|
343
|
+
projectKey: backendProjectKey,
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
checks.push(
|
|
347
|
+
buildCheck({
|
|
348
|
+
id: "backend_ingest",
|
|
349
|
+
passed: Boolean(result.transportOk && result.response.ok),
|
|
350
|
+
result,
|
|
351
|
+
url: backendIngestUrl,
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
// Ingest 201 受領だけで「動いている」 と判定すると、raw-ingest→normalize→
|
|
355
|
+
// publish の下流 hop が壊れた状態を見逃す。ingest response から batch id
|
|
356
|
+
// を抽出し、backend_ingest_reached_ledger で published まで検証する。
|
|
357
|
+
if (result.transportOk && result.response.ok) {
|
|
358
|
+
backendIngestBatchId = observationBatchIdFromResult(result);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Confirm backend ingest reached published state, not only HTTP 201.
|
|
363
|
+
if (
|
|
364
|
+
!backendIngestBatchId ||
|
|
365
|
+
!cluebaseApiBaseUrl ||
|
|
366
|
+
!apiKey ||
|
|
367
|
+
!backendProjectKey
|
|
368
|
+
) {
|
|
369
|
+
checks.push(
|
|
370
|
+
requiredInputCheck({
|
|
371
|
+
id: "backend_ingest_reached_ledger",
|
|
372
|
+
missing: [
|
|
373
|
+
...(!backendIngestBatchId ? ["backend_ingest_batch_id"] : []),
|
|
374
|
+
...(!cluebaseApiBaseUrl ? ["api-base-url"] : []),
|
|
375
|
+
...(!apiKey ? ["CLUEBASE_API_KEY"] : []),
|
|
376
|
+
...(!backendProjectKey ? ["CLUEBASE_PROJECT_KEY"] : []),
|
|
377
|
+
],
|
|
378
|
+
url: cluebaseApiBaseUrl
|
|
379
|
+
? joinUrl(cluebaseApiBaseUrl, BATCH_STATUS_PLACEHOLDER_PATH)
|
|
380
|
+
: null,
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
checks.push({
|
|
385
|
+
...(await pollBatchVisibility({
|
|
386
|
+
apiKey,
|
|
387
|
+
batchId: backendIngestBatchId,
|
|
388
|
+
checkId: "backend_ingest_reached_ledger",
|
|
389
|
+
cluebaseApiBaseUrl,
|
|
390
|
+
fetchImpl,
|
|
391
|
+
projectKey: backendProjectKey,
|
|
392
|
+
timeoutMs: batchVisibilityTimeoutConfig.timeoutMs,
|
|
393
|
+
})),
|
|
394
|
+
remediation_prompt: null,
|
|
395
|
+
});
|
|
396
|
+
throwIfSetupDoctorDeadlineExceeded(fetchImpl.signal);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Customer backends must not expose Cluebase-specific browser-token routes.
|
|
401
|
+
const skipCustomerBackendRouteScan = flags.has("skip-cluebase-route-scan");
|
|
402
|
+
const customerBackendRouteScanRoots = backendApplicable
|
|
403
|
+
? resolveBackendRootCandidates({
|
|
404
|
+
flags,
|
|
405
|
+
manifest,
|
|
406
|
+
repoRoot: resolvedRepoRoot,
|
|
407
|
+
})
|
|
408
|
+
: [];
|
|
409
|
+
const customerBackendRouteFiles = [];
|
|
410
|
+
let customerBackendRouteScanTruncated = false;
|
|
411
|
+
let customerBackendRouteScannedFiles = 0;
|
|
412
|
+
let customerBackendRouteScanRan = false;
|
|
413
|
+
if (
|
|
414
|
+
!skipCustomerBackendRouteScan &&
|
|
415
|
+
customerBackendRouteScanRoots.length > 0
|
|
416
|
+
) {
|
|
417
|
+
customerBackendRouteScanRan = true;
|
|
418
|
+
for (const rootPath of customerBackendRouteScanRoots) {
|
|
419
|
+
const result = await fetchImpl.runStage(() =>
|
|
420
|
+
collectCustomerBackendCluebaseRouteFiles({
|
|
421
|
+
repoRoot: resolvedRepoRoot,
|
|
422
|
+
rootAbs: resolve(resolvedRepoRoot, rootPath || "."),
|
|
423
|
+
signal: fetchImpl.signal,
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
customerBackendRouteScannedFiles += result.scanned;
|
|
427
|
+
if (result.truncated) customerBackendRouteScanTruncated = true;
|
|
428
|
+
for (const filePath of result.files) {
|
|
429
|
+
if (!customerBackendRouteFiles.includes(filePath))
|
|
430
|
+
customerBackendRouteFiles.push(filePath);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const customerBackendRouteScanMissing =
|
|
435
|
+
!skipCustomerBackendRouteScan && customerBackendRouteScanRoots.length === 0;
|
|
436
|
+
const customerBackendRouteCheck = !backendApplicable
|
|
437
|
+
? frontendOnlyBackendNaCheck("customer_backend_cluebase_route_forbidden")
|
|
438
|
+
: {
|
|
439
|
+
id: "customer_backend_cluebase_route_forbidden",
|
|
440
|
+
severity: "error",
|
|
441
|
+
method: null,
|
|
442
|
+
url: null,
|
|
443
|
+
passed:
|
|
444
|
+
!skipCustomerBackendRouteScan &&
|
|
445
|
+
!customerBackendRouteScanMissing &&
|
|
446
|
+
!customerBackendRouteScanTruncated &&
|
|
447
|
+
customerBackendRouteFiles.length === 0,
|
|
448
|
+
status:
|
|
449
|
+
customerBackendRouteScanMissing || skipCustomerBackendRouteScan
|
|
450
|
+
? "skipped"
|
|
451
|
+
: null,
|
|
452
|
+
error: skipCustomerBackendRouteScan
|
|
453
|
+
? "Customer-backend Cluebase route scan was explicitly skipped; setup cannot be marked passed until direct browser-token routing is verified."
|
|
454
|
+
: customerBackendRouteScanMissing
|
|
455
|
+
? "customer backend root was not found; Customer-backend Cluebase route scan did not run. Pass --backend-root-path or provide .cluebase/setup-manifest.json detected.backend_root_path to verify direct browser-token routing."
|
|
456
|
+
: customerBackendRouteScanTruncated
|
|
457
|
+
? "Customer-backend Cluebase route scan was incomplete: it reached its file limit or could not read a configured root/file. Narrow --backend-root-path, remove generated files, or fix the unreadable path so direct browser-token routing can be verified."
|
|
458
|
+
: customerBackendRouteFiles.length === 0
|
|
459
|
+
? null
|
|
460
|
+
: "Customer-backend Cluebase route detected. The frontend SDK calls the Cluebase backend directly; remove this Cluebase route before setup can pass.",
|
|
461
|
+
files: customerBackendRouteFiles,
|
|
462
|
+
scan_ran: customerBackendRouteScanRan,
|
|
463
|
+
scan_truncated: customerBackendRouteScanTruncated,
|
|
464
|
+
scanned_files: customerBackendRouteScannedFiles,
|
|
465
|
+
scan_root_candidates: customerBackendRouteScanRoots.map((root) => root || "."),
|
|
466
|
+
};
|
|
467
|
+
checks.push(customerBackendRouteCheck);
|
|
468
|
+
|
|
469
|
+
// 顧客 source code / discoveries.json / env file を解析し、 API 疎通だけでは
|
|
470
|
+
// 検知不能な「動いているのに使えない」 失敗を fail-fast で halt する。
|
|
471
|
+
//
|
|
472
|
+
// 起動条件: `.cluebase/discoveries.json` が存在する顧客 repo 文脈でのみ実行。
|
|
473
|
+
// discoveries.json が無い文脈 (= test 用 tmpdir / CLI 単体起動) では quality
|
|
474
|
+
// check 自体を skip し、 API 疎通だけ検証する。
|
|
475
|
+
// `--skip-quality-checks` flag で明示的に stop 可 (= 強制 skip)。
|
|
476
|
+
// `--force-quality-checks` flag で discoveries.json が無くても実行 (= test 用)。
|
|
477
|
+
let qualityChecks = [];
|
|
478
|
+
let qualityCheckSummary = null;
|
|
479
|
+
const skipQuality = flags.has("skip-quality-checks");
|
|
480
|
+
const forceQuality = flags.has("force-quality-checks");
|
|
481
|
+
if (!skipQuality) {
|
|
482
|
+
const qualityInputs = await fetchImpl.runStage(() =>
|
|
483
|
+
loadQualityInputs({ repoRoot: resolvedRepoRoot, signal: fetchImpl.signal }),
|
|
484
|
+
);
|
|
485
|
+
const hasDiscoveries = qualityInputs.discoveries !== null;
|
|
486
|
+
if (hasDiscoveries || forceQuality) {
|
|
487
|
+
const qualityResult = await fetchImpl.runStage(() =>
|
|
488
|
+
runQualityChecks({
|
|
489
|
+
...qualityInputs,
|
|
490
|
+
repoRoot: resolvedRepoRoot,
|
|
491
|
+
signal: fetchImpl.signal,
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
qualityChecks = qualityResult.checks;
|
|
495
|
+
qualityCheckSummary = qualityResult.summary;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// 集計: API 疎通 + customer route surface check + quality error check 全 pass で setup 成功。
|
|
499
|
+
// quality warn check は続行可。
|
|
500
|
+
const errorChecks = checks.filter((check) => check.severity === "error");
|
|
501
|
+
const warningChecks = checks.filter((check) => check.severity === "warning");
|
|
502
|
+
const apiConnectivityPassed = errorChecks.every((check) => check.passed);
|
|
503
|
+
const qualityErrorPassed = qualityCheckSummary
|
|
504
|
+
? qualityCheckSummary.errorFailed === 0
|
|
505
|
+
: true;
|
|
506
|
+
const passed = apiConnectivityPassed && qualityErrorPassed;
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
status: passed ? "passed" : "failed",
|
|
510
|
+
passed,
|
|
511
|
+
contract: API_CONNECTIVITY_CONTRACT,
|
|
512
|
+
checks,
|
|
513
|
+
qualityChecks,
|
|
514
|
+
qualityCheckSummary,
|
|
515
|
+
summary: {
|
|
516
|
+
apiConnectivity: {
|
|
517
|
+
total: errorChecks.length,
|
|
518
|
+
failed: errorChecks.filter((c) => !c.passed).length,
|
|
519
|
+
passed: apiConnectivityPassed,
|
|
520
|
+
},
|
|
521
|
+
doctrineWarnings: {
|
|
522
|
+
total: warningChecks.length,
|
|
523
|
+
failed: warningChecks.filter((c) => !c.passed).length,
|
|
524
|
+
},
|
|
525
|
+
dataQuality: qualityCheckSummary,
|
|
526
|
+
},
|
|
527
|
+
inputs: {
|
|
528
|
+
manifest_loaded: Boolean(manifest),
|
|
529
|
+
client_frontend_url_configured: Boolean(clientFrontendUrl),
|
|
530
|
+
origin_configured: Boolean(origin),
|
|
531
|
+
cluebase_api_base_url_configured: Boolean(cluebaseApiBaseUrl),
|
|
532
|
+
project_key_configured: Boolean(projectKey),
|
|
533
|
+
frontend_project_key_configured: Boolean(frontendProjectKey),
|
|
534
|
+
backend_project_key_configured: Boolean(backendProjectKey),
|
|
535
|
+
frontend_cluebase_api_base_url_configured: Boolean(frontendCluebaseApiBaseUrl),
|
|
536
|
+
backend_ingest_url_configured: Boolean(backendIngestUrl),
|
|
537
|
+
environment_configured: Boolean(environment),
|
|
538
|
+
backend_service_key_configured: Boolean(backendServiceKey),
|
|
539
|
+
cluebase_api_key_configured: Boolean(apiKey),
|
|
540
|
+
env_files_loaded: doctorEnv.loadedFiles,
|
|
541
|
+
frontend_env_files_loaded: doctorEnv.frontendLoadedFiles,
|
|
542
|
+
backend_env_files_loaded: doctorEnv.backendLoadedFiles,
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
}, API_CONNECTIVITY_CONTRACT);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export const SETUP_DOCUMENTATION_CONTRACT_VERSION = "1";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_SETUP_DOCUMENTS_URL = "/documents";
|
|
4
|
+
|
|
5
|
+
// Cluebase web app route + screen heading for the external data source connection
|
|
6
|
+
// screen. Value milestones that live only in an external system the customer app
|
|
7
|
+
// never sees (a billing platform, a CRM, a spreadsheet) cannot be instrumented
|
|
8
|
+
// with cluebase.track; they are ingested by connecting the external system on this
|
|
9
|
+
// screen. Kept next to DEFAULT_SETUP_DOCUMENTS_URL because both are Cluebase web app
|
|
10
|
+
// relative paths the setup flow points the customer to. Grounded in the web
|
|
11
|
+
// routing: sidebar "Settings" → "外部データ" nav item, page heading "外部データ接続".
|
|
12
|
+
export const EXTERNAL_DATA_CONNECTION_PATH = "/settings/external-data";
|
|
13
|
+
export const EXTERNAL_DATA_CONNECTION_SCREEN_NAME = "外部データ接続";
|
|
14
|
+
|
|
15
|
+
export const CORE_SETUP_DOCUMENT_IDS = [
|
|
16
|
+
"ai-setup-order",
|
|
17
|
+
"cluebase-boundary",
|
|
18
|
+
"environment-and-secrets",
|
|
19
|
+
"find-integration-points",
|
|
20
|
+
"official-sdk-contract",
|
|
21
|
+
"cluebase-init",
|
|
22
|
+
"cluebase-identify",
|
|
23
|
+
"cluebase-group-organization",
|
|
24
|
+
"cluebase-reset",
|
|
25
|
+
"cluebase-local-verification",
|
|
26
|
+
"forbidden-patterns",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const FRAMEWORK_SETUP_DOCUMENT_IDS = {
|
|
30
|
+
angular: "framework-angular",
|
|
31
|
+
django: "framework-django",
|
|
32
|
+
fastapi: "framework-fastapi",
|
|
33
|
+
nextjs: "framework-nextjs",
|
|
34
|
+
react: "framework-react-spa",
|
|
35
|
+
vite: "framework-react-spa",
|
|
36
|
+
vue: "framework-react-spa",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const optionalString = (value) =>
|
|
40
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
41
|
+
|
|
42
|
+
const normalizeDocumentsUrl = (documentsUrl) =>
|
|
43
|
+
optionalString(documentsUrl)?.replace(/\/+$/, "") ??
|
|
44
|
+
DEFAULT_SETUP_DOCUMENTS_URL;
|
|
45
|
+
|
|
46
|
+
const normalizeFrameworks = (frameworks = []) => [
|
|
47
|
+
...new Set(
|
|
48
|
+
frameworks
|
|
49
|
+
.map((framework) =>
|
|
50
|
+
typeof framework === "string" && framework.trim()
|
|
51
|
+
? framework.trim().toLowerCase()
|
|
52
|
+
: null,
|
|
53
|
+
)
|
|
54
|
+
.filter(Boolean),
|
|
55
|
+
),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const docUrlFor = ({ documentsUrl, docId }) => `${documentsUrl}#${docId}`;
|
|
59
|
+
|
|
60
|
+
export const frameworkDocIdsFor = (frameworks = []) => [
|
|
61
|
+
...new Set(
|
|
62
|
+
normalizeFrameworks(frameworks)
|
|
63
|
+
.map((framework) => FRAMEWORK_SETUP_DOCUMENT_IDS[framework])
|
|
64
|
+
.filter(Boolean),
|
|
65
|
+
),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
export const buildSetupDocumentationContract = ({
|
|
69
|
+
documentsUrl,
|
|
70
|
+
framework,
|
|
71
|
+
frameworks = [],
|
|
72
|
+
} = {}) => {
|
|
73
|
+
const normalizedDocumentsUrl = normalizeDocumentsUrl(documentsUrl);
|
|
74
|
+
const selectedFrameworks = normalizeFrameworks([
|
|
75
|
+
...(framework ? [framework] : []),
|
|
76
|
+
...frameworks,
|
|
77
|
+
]);
|
|
78
|
+
const selectedFrameworkDocIds = frameworkDocIdsFor(selectedFrameworks);
|
|
79
|
+
const requiredDocIds = [
|
|
80
|
+
...new Set([...CORE_SETUP_DOCUMENT_IDS, ...selectedFrameworkDocIds]),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
version: SETUP_DOCUMENTATION_CONTRACT_VERSION,
|
|
85
|
+
documents_url: normalizedDocumentsUrl,
|
|
86
|
+
external_data_source_connection: {
|
|
87
|
+
screen_name: EXTERNAL_DATA_CONNECTION_SCREEN_NAME,
|
|
88
|
+
path: EXTERNAL_DATA_CONNECTION_PATH,
|
|
89
|
+
purpose:
|
|
90
|
+
"Ingest value data that no SDK call can capture because it lives only in an external system the customer app never sees (a billing platform, a CRM, a spreadsheet). The customer connects the external system on this Cluebase web screen instead of instrumenting cluebase.track.",
|
|
91
|
+
},
|
|
92
|
+
required_doc_ids: requiredDocIds,
|
|
93
|
+
framework_doc_ids_by_framework: FRAMEWORK_SETUP_DOCUMENT_IDS,
|
|
94
|
+
selected_frameworks: selectedFrameworks,
|
|
95
|
+
selected_framework_doc_ids: selectedFrameworkDocIds,
|
|
96
|
+
doc_urls: Object.fromEntries(
|
|
97
|
+
requiredDocIds.map((docId) => [
|
|
98
|
+
docId,
|
|
99
|
+
docUrlFor({ documentsUrl: normalizedDocumentsUrl, docId }),
|
|
100
|
+
]),
|
|
101
|
+
),
|
|
102
|
+
pre_editing_gate: [
|
|
103
|
+
"Open documents_url before editing when tool access allows it.",
|
|
104
|
+
"Read every required_doc_ids entry that applies to the detected framework.",
|
|
105
|
+
"If documents_url cannot be opened, continue only from the generated skills and manifest doc ids, and report documentation_access_blocked.",
|
|
106
|
+
"Do not implement by prompt memory alone when the manifest contains a documentation contract.",
|
|
107
|
+
],
|
|
108
|
+
report_required_fields: ["consulted_document_ids"],
|
|
109
|
+
agent_rule:
|
|
110
|
+
"Read the relevant Cluebase setup documents before editing and list consulted_document_ids in the final report.",
|
|
111
|
+
};
|
|
112
|
+
};
|