@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.
Files changed (38) hide show
  1. package/README.md +101 -0
  2. package/bin/cluebase-cli.mjs +11 -0
  3. package/package.json +17 -0
  4. package/src/cli-command.mjs +515 -0
  5. package/src/cli-invocation.mjs +17 -0
  6. package/src/code-evidence-analyzer.mjs +2041 -0
  7. package/src/contracts.mjs +36 -0
  8. package/src/generated-code-evidence-contract.mjs +22 -0
  9. package/src/generated-sdk-version-contract.mjs +5 -0
  10. package/src/generated-source-path-policy.mjs +20 -0
  11. package/src/lifecycle-guard.mjs +202 -0
  12. package/src/path-policy.mjs +81 -0
  13. package/src/setup-ai-contract.mjs +221 -0
  14. package/src/setup-check-constants.mjs +110 -0
  15. package/src/setup-check-scan-a.mjs +849 -0
  16. package/src/setup-check-scan-b.mjs +994 -0
  17. package/src/setup-check.mjs +575 -0
  18. package/src/setup-discover-check.mjs +755 -0
  19. package/src/setup-doctor-deadline.mjs +221 -0
  20. package/src/setup-doctor-env.mjs +331 -0
  21. package/src/setup-doctor-file-boundary.mjs +426 -0
  22. package/src/setup-doctor-probe.mjs +719 -0
  23. package/src/setup-doctor-quality-checks-a.mjs +593 -0
  24. package/src/setup-doctor-quality-checks-b.mjs +638 -0
  25. package/src/setup-doctor-quality-shared.mjs +382 -0
  26. package/src/setup-doctor-quality.mjs +209 -0
  27. package/src/setup-doctor-route-scan.mjs +160 -0
  28. package/src/setup-doctor-sdk-probe.mjs +340 -0
  29. package/src/setup-doctor.mjs +545 -0
  30. package/src/setup-documents.mjs +112 -0
  31. package/src/setup-help.mjs +130 -0
  32. package/src/setup-prepare.mjs +360 -0
  33. package/src/setup-repository-discovery.mjs +764 -0
  34. package/src/setup-step-builders-discover.mjs +701 -0
  35. package/src/setup-step-builders-events.mjs +229 -0
  36. package/src/setup-step-builders-implement.mjs +710 -0
  37. package/src/setup-step-commands.mjs +427 -0
  38. 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
+ };