@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,36 @@
|
|
|
1
|
+
// 環境別 key (Stripe 流) の prefix から environment を導出する。
|
|
2
|
+
// apps/api/src/common/security/project-api-key.crypto.ts の
|
|
3
|
+
// deriveEnvironmentFromProjectKey と完全一致するロジック。
|
|
4
|
+
//
|
|
5
|
+
// - `pk_dev_` で始まる project_key は環境 "dev"
|
|
6
|
+
// - `pk_prod_` で始まる project_key は環境 "prod"
|
|
7
|
+
// - 上記以外の prefix は null を返す → caller が早期 error 化
|
|
8
|
+
//
|
|
9
|
+
// SPEC: docs/contracts/project-keys/environment-prefixes.md
|
|
10
|
+
export const PROJECT_KEY_ENVIRONMENT_PREFIX = Object.freeze({
|
|
11
|
+
dev: "pk_dev_",
|
|
12
|
+
prod: "pk_prod_",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const PROJECT_API_KEY_ENVIRONMENT_PREFIX = Object.freeze({
|
|
16
|
+
dev: "ak_dev_",
|
|
17
|
+
prod: "ak_prod_",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const deriveEnvironmentFromProjectKey = (projectKey) => {
|
|
21
|
+
if (typeof projectKey !== "string") return null;
|
|
22
|
+
for (const [env, prefix] of Object.entries(PROJECT_KEY_ENVIRONMENT_PREFIX)) {
|
|
23
|
+
if (projectKey.startsWith(prefix)) return env;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const deriveEnvironmentFromProjectApiKey = (apiKey) => {
|
|
29
|
+
if (typeof apiKey !== "string") return null;
|
|
30
|
+
for (const [env, prefix] of Object.entries(
|
|
31
|
+
PROJECT_API_KEY_ENVIRONMENT_PREFIX,
|
|
32
|
+
)) {
|
|
33
|
+
if (apiKey.startsWith(prefix)) return env;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Generated from packages/cluebase-schema/src/github/code-evidence.schema.ts.
|
|
2
|
+
// Run `node scripts/generate-code-evidence-contract.mjs` after changing the schema.
|
|
3
|
+
export const CODE_EVIDENCE_ANALYZER_VERSION = "code-evidence-v1";
|
|
4
|
+
export const CODE_EVIDENCE_OPERATION_DISCRIMINATOR_FIELD = "action";
|
|
5
|
+
export const CODE_EVIDENCE_ROUTE_METHODS = Object.freeze([
|
|
6
|
+
"GET",
|
|
7
|
+
"POST",
|
|
8
|
+
"PUT",
|
|
9
|
+
"PATCH",
|
|
10
|
+
"DELETE",
|
|
11
|
+
"OPTIONS",
|
|
12
|
+
"HEAD",
|
|
13
|
+
"TRACE",
|
|
14
|
+
"ALL"
|
|
15
|
+
]);
|
|
16
|
+
export const CODE_EVIDENCE_DATA_OPERATIONS = Object.freeze([
|
|
17
|
+
"read",
|
|
18
|
+
"create",
|
|
19
|
+
"update",
|
|
20
|
+
"delete",
|
|
21
|
+
"unknown"
|
|
22
|
+
]);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Generated from the official SDK package versions.
|
|
2
|
+
// Run `node scripts/generate-sdk-version-contract.mjs` after changing an SDK version.
|
|
3
|
+
export const RECOMMENDED_BACKEND_SDK_VERSION = "0.0.1";
|
|
4
|
+
export const RECOMMENDED_BACKEND_SDK_PACKAGE_SPEC = "@genn-inc/cluebase-backend-sdk";
|
|
5
|
+
export const RECOMMENDED_PYTHON_BACKEND_SDK_PACKAGE_SPEC = "cluebase-backend-sdk";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated from packages/cluebase-schema/src/source-path-policy.ts.
|
|
2
|
+
// Regenerate with node scripts/generate-source-path-policy.mjs after changing the schema policy.
|
|
3
|
+
const DEFAULT_EXCLUDED_PATH_PARTS = Object.freeze([".git",".env",".venv",".next","venv","site-packages","node_modules","dist","build","coverage","logs","tmp","vendor","credentials","secrets","__pycache__"]);
|
|
4
|
+
const DEFAULT_EXCLUDED_PATH_PREFIXES = Object.freeze([".venv-",".venv_","venv-","venv_"]);
|
|
5
|
+
const SENSITIVE_PATH_PART_PATTERN = /^(?:\.env(?:[._-].*)?|credentials(?:[._-].*)?|secrets(?:[._-].*)?)$/i;
|
|
6
|
+
|
|
7
|
+
export function isExcludedSourcePath(relativePath, excludedPathParts = []) {
|
|
8
|
+
const excluded = new Set([
|
|
9
|
+
...DEFAULT_EXCLUDED_PATH_PARTS,
|
|
10
|
+
...excludedPathParts,
|
|
11
|
+
]);
|
|
12
|
+
return relativePath
|
|
13
|
+
.split(/[\\/]+/)
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.some((part) =>
|
|
16
|
+
excluded.has(part) ||
|
|
17
|
+
SENSITIVE_PATH_PART_PATTERN.test(part) ||
|
|
18
|
+
DEFAULT_EXCLUDED_PATH_PREFIXES.some((prefix) => part.startsWith(prefix)),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const LIFECYCLE_CALL_PATTERN =
|
|
2
|
+
/\b(cluebase\.init|cluebase_init_fastapi|cluebase\.identify|cluebase\.group|cluebase\.reset|cluebase\.track)\s*\(/g;
|
|
3
|
+
|
|
4
|
+
export const stripSourceNoise = (text, { stripStrings = false } = {}) => {
|
|
5
|
+
let output = "";
|
|
6
|
+
let index = 0;
|
|
7
|
+
while (index < text.length) {
|
|
8
|
+
const char = text[index];
|
|
9
|
+
const next = text[index + 1];
|
|
10
|
+
if (char === "/" && next === "/") {
|
|
11
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (char === "#") {
|
|
15
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (char === "/" && next === "*") {
|
|
19
|
+
index += 2;
|
|
20
|
+
while (
|
|
21
|
+
index < text.length &&
|
|
22
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
23
|
+
) {
|
|
24
|
+
if (text[index] === "\n") output += "\n";
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
index += index < text.length ? 2 : 0;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
31
|
+
const quote = char;
|
|
32
|
+
const triple =
|
|
33
|
+
quote !== "`" && text.slice(index, index + 3) === quote.repeat(3);
|
|
34
|
+
const endToken = triple ? quote.repeat(3) : quote;
|
|
35
|
+
if (!stripStrings) {
|
|
36
|
+
output += triple ? endToken : quote;
|
|
37
|
+
} else {
|
|
38
|
+
output += quote === "`" ? "``" : `${quote}${quote}`;
|
|
39
|
+
}
|
|
40
|
+
index += triple ? 3 : 1;
|
|
41
|
+
while (index < text.length) {
|
|
42
|
+
if (!triple && text[index] === "\\") {
|
|
43
|
+
if (!stripStrings) output += text.slice(index, index + 2);
|
|
44
|
+
index += 2;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (text.slice(index, index + endToken.length) === endToken) {
|
|
48
|
+
if (!stripStrings) output += endToken;
|
|
49
|
+
index += endToken.length;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (text[index] === "\n") output += "\n";
|
|
53
|
+
if (!stripStrings && text[index] !== "\n") output += text[index];
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
output += char;
|
|
59
|
+
index += 1;
|
|
60
|
+
}
|
|
61
|
+
return output;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const isIdentifierCharacter = (character) =>
|
|
65
|
+
typeof character === "string" && /[A-Za-z0-9_$]/.test(character);
|
|
66
|
+
|
|
67
|
+
const startsKeyword = (text, index, keyword) =>
|
|
68
|
+
text.startsWith(keyword, index) &&
|
|
69
|
+
!isIdentifierCharacter(text[index - 1]) &&
|
|
70
|
+
!isIdentifierCharacter(text[index + keyword.length]);
|
|
71
|
+
|
|
72
|
+
const advanceQuotedString = (text, startIndex, { preserve = false } = {}) => {
|
|
73
|
+
const quote = text[startIndex];
|
|
74
|
+
const triple = quote !== "`" && text.slice(startIndex, startIndex + 3) === quote.repeat(3);
|
|
75
|
+
const endToken = triple ? quote.repeat(3) : quote;
|
|
76
|
+
let index = startIndex + (triple ? 3 : 1);
|
|
77
|
+
let value = preserve ? (triple ? endToken : quote) : "";
|
|
78
|
+
while (index < text.length) {
|
|
79
|
+
if (!triple && text[index] === "\\") {
|
|
80
|
+
if (preserve) value += text.slice(index, index + 2);
|
|
81
|
+
index += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (text.slice(index, index + endToken.length) === endToken) {
|
|
85
|
+
if (preserve) value += endToken;
|
|
86
|
+
index += endToken.length;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (preserve) value += text[index];
|
|
90
|
+
index += 1;
|
|
91
|
+
}
|
|
92
|
+
return { index, value };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const extractExecutableModuleStatements = (text) => {
|
|
96
|
+
const statements = [];
|
|
97
|
+
let index = 0;
|
|
98
|
+
while (index < text.length) {
|
|
99
|
+
const char = text[index];
|
|
100
|
+
const next = text[index + 1];
|
|
101
|
+
if (char === "/" && next === "/") {
|
|
102
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (char === "#") {
|
|
106
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (char === "/" && next === "*") {
|
|
110
|
+
index += 2;
|
|
111
|
+
while (
|
|
112
|
+
index < text.length &&
|
|
113
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
114
|
+
) {
|
|
115
|
+
index += 1;
|
|
116
|
+
}
|
|
117
|
+
index += index < text.length ? 2 : 0;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
121
|
+
index = advanceQuotedString(text, index).index;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!startsKeyword(text, index, "import") && !startsKeyword(text, index, "export")) {
|
|
125
|
+
index += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let statement = "";
|
|
129
|
+
let depth = 0;
|
|
130
|
+
while (index < text.length) {
|
|
131
|
+
const current = text[index];
|
|
132
|
+
const following = text[index + 1];
|
|
133
|
+
if (current === "/" && following === "/") {
|
|
134
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (current === "/" && following === "*") {
|
|
138
|
+
index += 2;
|
|
139
|
+
while (
|
|
140
|
+
index < text.length &&
|
|
141
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
142
|
+
) {
|
|
143
|
+
index += 1;
|
|
144
|
+
}
|
|
145
|
+
index += index < text.length ? 2 : 0;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (current === "'" || current === '"' || current === "`") {
|
|
149
|
+
const quoted = advanceQuotedString(text, index, { preserve: true });
|
|
150
|
+
statement += quoted.value;
|
|
151
|
+
index = quoted.index;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
statement += current;
|
|
155
|
+
if (current === "{" || current === "(" || current === "[") depth += 1;
|
|
156
|
+
if (current === "}" || current === ")" || current === "]") {
|
|
157
|
+
depth = Math.max(0, depth - 1);
|
|
158
|
+
}
|
|
159
|
+
index += 1;
|
|
160
|
+
if (current === ";" && depth === 0) break;
|
|
161
|
+
if (current === "\n" && depth === 0) break;
|
|
162
|
+
}
|
|
163
|
+
const trimmed = statement.trim();
|
|
164
|
+
if (trimmed) statements.push(trimmed);
|
|
165
|
+
}
|
|
166
|
+
return statements;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const lineNumberForIndex = (text, index) =>
|
|
170
|
+
text.slice(0, index).split("\n").length;
|
|
171
|
+
|
|
172
|
+
const isAwaitedOnCallLine = (text, callIndex) => {
|
|
173
|
+
const lineStart = text.lastIndexOf("\n", callIndex - 1) + 1;
|
|
174
|
+
return /\bawait\b/.test(text.slice(lineStart, callIndex));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const canonicalLifecycleApiName = (apiName) =>
|
|
178
|
+
apiName === "cluebase_init_fastapi" ? "cluebase.init" : apiName;
|
|
179
|
+
|
|
180
|
+
export const findLifecycleGuardViolations = (text) => {
|
|
181
|
+
const violations = [];
|
|
182
|
+
for (const match of text.matchAll(LIFECYCLE_CALL_PATTERN)) {
|
|
183
|
+
const callIndex = match.index ?? 0;
|
|
184
|
+
const apiName = canonicalLifecycleApiName(match[1]);
|
|
185
|
+
if (isAwaitedOnCallLine(text, callIndex)) {
|
|
186
|
+
violations.push({
|
|
187
|
+
api_name: apiName,
|
|
188
|
+
line: lineNumberForIndex(text, callIndex),
|
|
189
|
+
reason: "awaited_lifecycle_call",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return violations;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const findLifecycleCallApiNames = (text) => [
|
|
197
|
+
...new Set(
|
|
198
|
+
[...text.matchAll(LIFECYCLE_CALL_PATTERN)].map((match) =>
|
|
199
|
+
canonicalLifecycleApiName(match[1]),
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { isExcludedSourcePath as isExcludedSourcePathFromSchema } from "./generated-source-path-policy.mjs";
|
|
4
|
+
|
|
5
|
+
export const isExcludedSourcePath = (
|
|
6
|
+
relativePath,
|
|
7
|
+
excludedSourcePaths = [],
|
|
8
|
+
) => {
|
|
9
|
+
return isExcludedSourcePathFromSchema(relativePath, excludedSourcePaths);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const isInsideRoot = (root, absolutePath) => {
|
|
13
|
+
const relativePath = relative(root, absolutePath);
|
|
14
|
+
return (
|
|
15
|
+
relativePath === "" ||
|
|
16
|
+
(!relativePath.startsWith("..") && !isAbsolute(relativePath))
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const listAllowedSourceFiles = async ({
|
|
21
|
+
repoRoot,
|
|
22
|
+
allowedSourcePaths,
|
|
23
|
+
excludedSourcePaths,
|
|
24
|
+
extensions,
|
|
25
|
+
}) => {
|
|
26
|
+
const root = resolve(repoRoot);
|
|
27
|
+
const normalizedAllowedSourcePaths =
|
|
28
|
+
Array.isArray(allowedSourcePaths) && allowedSourcePaths.length > 0
|
|
29
|
+
? allowedSourcePaths
|
|
30
|
+
: ["."];
|
|
31
|
+
const normalizedExcludedSourcePaths = Array.isArray(excludedSourcePaths)
|
|
32
|
+
? excludedSourcePaths
|
|
33
|
+
: [];
|
|
34
|
+
const files = [];
|
|
35
|
+
const allowedExtensions = new Set(extensions);
|
|
36
|
+
|
|
37
|
+
const walk = async (absolutePath) => {
|
|
38
|
+
const relativePath = relative(root, absolutePath);
|
|
39
|
+
if (
|
|
40
|
+
relativePath &&
|
|
41
|
+
isExcludedSourcePathFromSchema(
|
|
42
|
+
relativePath,
|
|
43
|
+
normalizedExcludedSourcePaths,
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const currentStat = await lstat(absolutePath);
|
|
49
|
+
if (currentStat.isSymbolicLink()) {
|
|
50
|
+
throw new Error(`symbolic links are not allowed in source paths: ${relativePath}`);
|
|
51
|
+
}
|
|
52
|
+
if (currentStat.isDirectory()) {
|
|
53
|
+
const entries = await readdir(absolutePath);
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
await walk(join(absolutePath, entry));
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
currentStat.isFile() &&
|
|
61
|
+
[...allowedExtensions].some((extension) =>
|
|
62
|
+
absolutePath.endsWith(extension),
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
files.push(absolutePath);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (const sourcePath of normalizedAllowedSourcePaths) {
|
|
70
|
+
const absolutePath = resolve(root, sourcePath);
|
|
71
|
+
if (!isInsideRoot(root, absolutePath)) {
|
|
72
|
+
throw new Error(`allowed_source_paths escapes repo root: ${sourcePath}`);
|
|
73
|
+
}
|
|
74
|
+
await walk(absolutePath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [...new Set(files)].sort();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const listAllowedPythonFiles = async (options) =>
|
|
81
|
+
listAllowedSourceFiles({ ...options, extensions: [".py"] });
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
export const AI_SETUP_CONTRACT_VERSION =
|
|
2
|
+
"1";
|
|
3
|
+
|
|
4
|
+
export const SETUP_DOCTRINE = {
|
|
5
|
+
purpose:
|
|
6
|
+
"Cluebase setup installs an external SDK integration so observed product facts can reach Cluebase's Customer Value Understanding Engine. It is not an opportunity to improve, refactor, redesign, or reinterpret the host application.",
|
|
7
|
+
minimal_diff_reason:
|
|
8
|
+
"The customer must be able to review and merge the setup diff with confidence. Extra formatting, refactors, auth rewrites, UI changes, or unrelated cleanup make the integration harder to trust.",
|
|
9
|
+
ai_decision_boundary:
|
|
10
|
+
"The AI should use repository understanding only to choose existing lifecycle boundaries for cluebase.init, cluebase.identify, cluebase.group, and cluebase.reset.",
|
|
11
|
+
deterministic_control_boundary:
|
|
12
|
+
"Everything that can be controlled mechanically should be controlled by the CLI, generated skills, documentation contract, setup manifest, generated STEP commands, and setup-check static guards.",
|
|
13
|
+
documentation_reason:
|
|
14
|
+
"SDK signatures, environment variable names, browser token behavior, and verification ownership are contracts. The AI must read the setup documents instead of relying on memory.",
|
|
15
|
+
failure_posture:
|
|
16
|
+
"For supported setup paths, prefer completing safe minimal Cluebase wiring and reporting unclear lifecycle points as warnings. Reserve blockers for truly unsupported frameworks, unavailable SDK contracts, missing AI configuration, or edits that cannot be applied without guessing outside Cluebase setup scope.",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const DETERMINISTIC_CONTROL_MODEL = {
|
|
20
|
+
ai_should_decide: [
|
|
21
|
+
"which existing bootstrap point owns cluebase.init",
|
|
22
|
+
"which existing one-time auth success boundary owns cluebase.identify",
|
|
23
|
+
"which existing organization/company resolution point owns cluebase.group",
|
|
24
|
+
"which existing logout/session reset point owns cluebase.reset",
|
|
25
|
+
"whether a lifecycle point is unclear and should be skipped with a warning instead of guessed",
|
|
26
|
+
"which additional repository files or plain-text searches should be inspected through CLI-owned read-only tools before lifecycle placement",
|
|
27
|
+
],
|
|
28
|
+
cli_should_control: [
|
|
29
|
+
"read-only repository exploration tools, allowed paths, file inventory, read limits, search limits, and secret/path exclusions",
|
|
30
|
+
"official SDK package names",
|
|
31
|
+
"supported Cluebase SDK package names from the official package contract",
|
|
32
|
+
"official public SDK function names and supported lifecycle API set",
|
|
33
|
+
"environment variable names produced by setup and consumed by setup code",
|
|
34
|
+
"machine-owned semantic workflow generation and verification",
|
|
35
|
+
"static rejection of known unsafe wiring such as leaked secrets, wrong SDKs, blocking lifecycle calls, and broad cluebase.track setup",
|
|
36
|
+
"local API connectivity preflight for the three required setup hops before real product-flow log verification",
|
|
37
|
+
"canonical frontend SDK adapter env names and initialization safety checks",
|
|
38
|
+
"static rejection of hardcoded cluebase.identify and cluebase.group identity values",
|
|
39
|
+
"static rejection of cluebase.identify in repeated auth helper paths unless the helper is proven to be the one-time auth success boundary",
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const IDENTITY_BOUNDARY_CONTRACT = {
|
|
44
|
+
purpose:
|
|
45
|
+
"cluebase.identify records the authoritative identity event. It belongs only at the real auth success boundary that can prove the user identity and preserve browser linkage context when available.",
|
|
46
|
+
valid_boundaries: [
|
|
47
|
+
"frontend-owned login, sign-up, OAuth callback, magic-link, or OTP verification success",
|
|
48
|
+
"backend-owned auth success when the backend is the first reliable place that knows authentication succeeded",
|
|
49
|
+
"token exchange only when that exchange completes initial authentication, not when it only refreshes or rotates tokens",
|
|
50
|
+
"backend-only auth success without browser context, reported as backend-only evidence instead of browser-joinable identity",
|
|
51
|
+
],
|
|
52
|
+
repeated_helper_exclusions: [
|
|
53
|
+
"token refresh",
|
|
54
|
+
"current-user or /me sync",
|
|
55
|
+
"session polling",
|
|
56
|
+
"request exchange helpers that only rotate or hydrate credentials",
|
|
57
|
+
"read hooks and query functions",
|
|
58
|
+
"repeated local helpers that run after identity is already established",
|
|
59
|
+
],
|
|
60
|
+
rule:
|
|
61
|
+
"Do not add cluebase.identify to repeated helper paths by default. A repeated helper may emit identity only when repository evidence proves it is the actual one-time auth success boundary.",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const GROUP_BOUNDARY_CONTRACT = {
|
|
65
|
+
purpose:
|
|
66
|
+
"cluebase.group records the active organization context. Setup must choose one organization-association owner per runtime surface instead of adding SDK calls to every place where the active organization becomes visible.",
|
|
67
|
+
owner_kinds: {
|
|
68
|
+
initial_context:
|
|
69
|
+
"Auth success that returns the initial active organization. Place immediately after cluebase.identify in that success branch.",
|
|
70
|
+
active_context_owner:
|
|
71
|
+
"Existing singleton provider, auth-state listener, or guarded active-context effect that observes user id + organization id/name and dedupes by userId:organizationId. This is the only owner for that surface when it observes active organization changes.",
|
|
72
|
+
context_change:
|
|
73
|
+
"Create, join, accept-invite, switch, or onboarding success handler. Use only when no active_context_owner on the same surface emits the same change.",
|
|
74
|
+
},
|
|
75
|
+
duplicate_owner_rule:
|
|
76
|
+
"Do not place cluebase.group in both a guarded active-context owner and create/join/switch handlers on the same surface. The SDK must not hide duplicate organization_associated rows caused by duplicate setup call sites.",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const OTEL_FIRST_SETUP_CONTRACT = {
|
|
80
|
+
purpose:
|
|
81
|
+
"OpenTelemetry is the primary collection path wherever OpenTelemetry provides the signal. Setup guidance must teach OpenTelemetry, OpenLLMetry, and GenAI semantic instrumentation first.",
|
|
82
|
+
rules: [
|
|
83
|
+
"For backend request, ORM/database, AI, agent, tool, MCP, and framework signals covered by OpenTelemetry or current GenAI semantic conventions, install/use the OTel-primary SDK helper or standard OpenTelemetry/OpenLLMetry instrumentor first.",
|
|
84
|
+
"Cluebase helpers may enrich, correlate, mask, classify, or provide a fallback for unavailable OTel libraries; they must not replace OTel as the primary source for an OTel-available signal.",
|
|
85
|
+
"Do not pass raw prompts, completions, tool arguments, request bodies, SQL, bind values, or resource bodies into Cluebase helper arguments.",
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const API_CONNECTIVITY_CONTRACT = {
|
|
90
|
+
purpose:
|
|
91
|
+
"Cluebase setup has three integration endpoints plus one setup-owned batch-status polling endpoint. setup-doctor probes the Cluebase endpoints with SDK-equivalent payloads and customer env files. The customer backend never carries a Cluebase browser-token route: the frontend SDK calls the Cluebase backend directly using the public project key, request Origin, and a short-lived browser token.",
|
|
92
|
+
hops: {
|
|
93
|
+
cluebase_backend_browser_token_issue: {
|
|
94
|
+
owner: "cluebase_backend",
|
|
95
|
+
method: "POST",
|
|
96
|
+
path: "/api/v1/ingest/browser-tokens",
|
|
97
|
+
caller: "customer_frontend_sdk",
|
|
98
|
+
purpose:
|
|
99
|
+
"Cluebase backend validates the project key and request Origin, then returns a short-lived browser token. Called directly by the frontend SDK; no customer backend route in between.",
|
|
100
|
+
},
|
|
101
|
+
browser_ingest: {
|
|
102
|
+
owner: "cluebase_backend",
|
|
103
|
+
method: "POST",
|
|
104
|
+
path: "/api/v1/ingest/browser",
|
|
105
|
+
caller: "customer_frontend_sdk",
|
|
106
|
+
env_name:
|
|
107
|
+
"framework-specific public CLUEBASE_API_BASE_URL name; Next.js uses NEXT_PUBLIC_CLUEBASE_API_BASE_URL",
|
|
108
|
+
purpose:
|
|
109
|
+
"Frontend SDK sends canonical observation source event batches with x-cluebase-browser-token.",
|
|
110
|
+
},
|
|
111
|
+
backend_ingest: {
|
|
112
|
+
owner: "cluebase_backend",
|
|
113
|
+
method: "POST",
|
|
114
|
+
path: "/api/v1/ingest/backend",
|
|
115
|
+
caller: "setup-doctor using backend SDK-equivalent payload",
|
|
116
|
+
env_name: "CLUEBASE_INGEST_ENDPOINT",
|
|
117
|
+
purpose:
|
|
118
|
+
"setup-doctor verifies backend ingest acceptance with server-side CLUEBASE_API_KEY. Real customer backend lifecycle emission is verified by product logs after the customer runs the local service.",
|
|
119
|
+
},
|
|
120
|
+
batch_status: {
|
|
121
|
+
owner: "cluebase_backend",
|
|
122
|
+
method: "GET",
|
|
123
|
+
path: "/api/v1/ingest/batch-status/:batchId",
|
|
124
|
+
caller: "setup-doctor",
|
|
125
|
+
env_name: "CLUEBASE_INGEST_ENDPOINT",
|
|
126
|
+
purpose:
|
|
127
|
+
"Setup verification polls accepted ingest batches until worker publish and durable publish evidence are proven.",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
universal_http_fallback:
|
|
131
|
+
"The three ingest endpoints are the stable contract; the Cluebase SDKs are a convenience over them. Any stack with no matching Cluebase SDK integrates by calling POST /api/v1/ingest/browser-tokens, POST /api/v1/ingest/browser (x-cluebase-browser-token), and POST /api/v1/ingest/backend (x-cluebase-api-key) directly. The Cluebase environment is derived from the project key prefix; do not add customer-backend Cluebase proxy routes.",
|
|
132
|
+
preflight_command: "setup-doctor --local",
|
|
133
|
+
verification_states: [
|
|
134
|
+
"accepted",
|
|
135
|
+
"received",
|
|
136
|
+
"completed",
|
|
137
|
+
"published",
|
|
138
|
+
],
|
|
139
|
+
product_flow_verification_boundary:
|
|
140
|
+
"setup-doctor verifies canonical ingest acceptance and records downstream publish evidence. Real customer frontend/backend lifecycle emission is verified from Cluebase setup logs after the customer runs the local product flow.",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const FRONTEND_ADAPTER_CONTRACT = {
|
|
144
|
+
purpose:
|
|
145
|
+
"Frontend SDK adapter code is part of the Cluebase setup contract. The AI may choose where the adapter is imported, but must not invent token URL, env, or initialization semantics. The frontend SDK calls the Cluebase backend directly.",
|
|
146
|
+
nextjs_public_env: [
|
|
147
|
+
"NEXT_PUBLIC_CLUEBASE_PROJECT_KEY",
|
|
148
|
+
"NEXT_PUBLIC_CLUEBASE_API_BASE_URL",
|
|
149
|
+
],
|
|
150
|
+
rules: [
|
|
151
|
+
"For Next.js App Router, prefer a client bootstrap module such as src/lib/cluebase.ts that calls cluebase.init once after required NEXT_PUBLIC_CLUEBASE_* values are present, exports a tiny Client Component such as CluebaseInit, and is rendered once from app/layout.tsx or the existing app bootstrap.",
|
|
152
|
+
"For non-Next.js browser/client code, use the framework-specific public env names generated by Cluebase setup instead of NEXT_PUBLIC_*.",
|
|
153
|
+
"Next.js frontend SDK adapter files must start with \"use client\".",
|
|
154
|
+
"Do not create a React component whose useEffect calls cluebase.init. Component lifecycle hooks, page components, sidebars, login/register success callbacks, and other repeated UI paths are rejected setup locations.",
|
|
155
|
+
"Do not call cluebase.init with empty-string fallbacks for required public Cluebase env values.",
|
|
156
|
+
"If a singleton guard is used, do not mark initialized=true before cluebase.init has been called with required config present.",
|
|
157
|
+
"Do not add a customer-backend route under /api/v1/cluebase/*. The frontend SDK calls the Cluebase backend directly for browser tokens.",
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const OFFICIAL_SDK_CONTRACT = {
|
|
162
|
+
purpose:
|
|
163
|
+
"This bundled contract is the authoritative Cluebase SDK contract for Cluebase setup wiring. A customer repository is expected to start without Cluebase SDK imports or dependencies; absence of existing Cluebase code is not a blocker.",
|
|
164
|
+
frontend_sdk_hop: {
|
|
165
|
+
package_name: "@genn-inc/cluebase-frontend-sdk",
|
|
166
|
+
dependency_specifier: "@genn-inc/cluebase-frontend-sdk@latest",
|
|
167
|
+
import_path: "@genn-inc/cluebase-frontend-sdk",
|
|
168
|
+
public_lifecycle_apis: {
|
|
169
|
+
"cluebase.init":
|
|
170
|
+
"cluebase.init(options: { endpoint: string; projectKey: string; ... }): void // environment (dev/prod) is derived from projectKey prefix (pk_dev_ vs pk_prod_). Browser token issuance is built in from the Cluebase API base URL, projectKey, and request Origin.",
|
|
171
|
+
"cluebase.identify":
|
|
172
|
+
"cluebase.identify(userId: string, traits?: Record<string, string | number | boolean | null>): void",
|
|
173
|
+
"cluebase.group":
|
|
174
|
+
'cluebase.group("organization", organizationId: string, traits?: Record<string, string | number | boolean | null>): void',
|
|
175
|
+
"cluebase.reset": "cluebase.reset(): void",
|
|
176
|
+
},
|
|
177
|
+
safety_contract:
|
|
178
|
+
"Public lifecycle APIs are no-throw wrappers. Do not add custom per-call try/catch or await them.",
|
|
179
|
+
},
|
|
180
|
+
backend_fastapi_sdk: {
|
|
181
|
+
package_name: "cluebase-backend-sdk",
|
|
182
|
+
dependency_specifier: "cluebase-backend-sdk",
|
|
183
|
+
python_import:
|
|
184
|
+
"from cluebase_backend_sdk._integrations.fastapi import cluebase_init_fastapi",
|
|
185
|
+
public_lifecycle_apis: {
|
|
186
|
+
"cluebase.init":
|
|
187
|
+
"cluebase_init_fastapi(app, project_key: str, api_key: str, service_key: str, ...) -> bool // environment (dev/prod) is derived from project_key prefix (pk_dev_ vs pk_prod_)",
|
|
188
|
+
"cluebase.identify":
|
|
189
|
+
"cluebase.identify(user_id: str, traits: Mapping[str, object] | None = None) -> None",
|
|
190
|
+
"cluebase.group":
|
|
191
|
+
'cluebase.group("organization", organization_id: str, traits: Mapping[str, object] | None = None) -> None',
|
|
192
|
+
"cluebase.reset": "cluebase.reset() -> None",
|
|
193
|
+
},
|
|
194
|
+
safety_contract:
|
|
195
|
+
"Public lifecycle APIs catch SDK errors internally; FastAPI initialization reports success as bool and namespace lifecycle calls return None. Do not wrap each call solely for Cluebase failure isolation. Backend initialization must include the required SDK options. Cluebase env reads must be non-crashing; do not use os.environ[\"CLUEBASE_*\"] required indexing.",
|
|
196
|
+
},
|
|
197
|
+
minimal_file_creation_contract: {
|
|
198
|
+
allowed_when:
|
|
199
|
+
"No existing Cluebase adapter or client bootstrap wrapper exists in the customer repo.",
|
|
200
|
+
allowed_files:
|
|
201
|
+
"Only Cluebase-owned minimal SDK wiring files under existing frontend/backend source roots, plus exact replacements in existing files to import/register those files.",
|
|
202
|
+
rule:
|
|
203
|
+
"Creating a minimal Cluebase adapter is allowed setup wiring, not a host application refactor. Customer-backend routes such as /api/v1/cluebase/* are outside the Cluebase setup contract because the frontend SDK calls the Cluebase backend directly.",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const setupDoctrineSkillLines = () => [
|
|
208
|
+
`- Purpose: ${SETUP_DOCTRINE.purpose}`,
|
|
209
|
+
`- Minimal diff reason: ${SETUP_DOCTRINE.minimal_diff_reason}`,
|
|
210
|
+
`- AI decision boundary: ${SETUP_DOCTRINE.ai_decision_boundary}`,
|
|
211
|
+
`- Static control boundary: ${SETUP_DOCTRINE.deterministic_control_boundary}`,
|
|
212
|
+
`- Documentation reason: ${SETUP_DOCTRINE.documentation_reason}`,
|
|
213
|
+
`- Failure posture: ${SETUP_DOCTRINE.failure_posture}`,
|
|
214
|
+
`- Group boundary: ${GROUP_BOUNDARY_CONTRACT.duplicate_owner_rule}`,
|
|
215
|
+
"- Read-only exploration boundary: the AI may choose additional repository files or plain-text searches, but the CLI owns the actual read/search tools, allowed paths, limits, and secret exclusions.",
|
|
216
|
+
`- Official SDK contract: ${OFFICIAL_SDK_CONTRACT.purpose}`,
|
|
217
|
+
`- API connectivity: ${API_CONNECTIVITY_CONTRACT.purpose}`,
|
|
218
|
+
`- Universal HTTP fallback: ${API_CONNECTIVITY_CONTRACT.universal_http_fallback}`,
|
|
219
|
+
`- Frontend adapter: ${FRONTEND_ADAPTER_CONTRACT.purpose}`,
|
|
220
|
+
`- API preflight: run ${API_CONNECTIVITY_CONTRACT.preflight_command} when local services and required env are available; then verify real product-flow event delivery from Cluebase setup logs or published batch evidence.`,
|
|
221
|
+
];
|