返回 CodeWhale
facts.ts
根目录 / web / lib / facts.ts
1 import {
2 FACTS as BUILD_TIME_FACTS,
3 type PublishedReleaseFact,
4 type RepoFacts,
5 type ProviderFact,
6 type ModelFact,
7 } from "./facts.generated";
8
9 const KV_KEY = "facts:current";
10
11 export type { PublishedReleaseFact, RepoFacts, ProviderFact, ModelFact };
12
13 export const BUILD_FACTS: RepoFacts = {
14 ...BUILD_TIME_FACTS,
15 sourceRevision:
16 process.env.NEXT_PUBLIC_CODEWHALE_SOURCE_REVISION || BUILD_TIME_FACTS.sourceRevision,
17 sourceCommittedAt:
18 process.env.NEXT_PUBLIC_CODEWHALE_SOURCE_COMMITTED_AT ||
19 BUILD_TIME_FACTS.sourceCommittedAt,
20 };
21
22 interface KVNamespace {
23 get(key: string): Promise<string | null>;
24 put(key: string, value: string, opts?: { expirationTtl?: number }): Promise<void>;
25 }
26
27 export type FactsSource = "build" | "kv";
28
29 export interface FactsResolution {
30 facts: RepoFacts;
31 source: FactsSource;
32 reason:
33 | "no-kv-snapshot"
34 | "invalid-kv-snapshot"
35 | "kv-missing-source-provenance"
36 | "build-missing-source-provenance"
37 | "same-source-revision"
38 | "kv-source-newer"
39 | "build-source-newer-or-ambiguous";
40 buildSourceRevision: string | null;
41 kvSourceRevision: string | null;
42 }
43
44 function isRecord(value: unknown): value is Record<string, unknown> {
45 return value !== null && typeof value === "object" && !Array.isArray(value);
46 }
47
48 function isModelFact(value: unknown): value is ModelFact {
49 if (!isRecord(value)) return false;
50 return (
51 typeof value.id === "string" &&
52 (value.provider === null || typeof value.provider === "string") &&
53 (value.contextWindow === null || typeof value.contextWindow === "number") &&
54 (value.maxOutput === null || typeof value.maxOutput === "number") &&
55 typeof value.reasoning === "boolean" &&
56 (value.addedAt === null || typeof value.addedAt === "string")
57 );
58 }
59
60 function isPublishedRelease(value: unknown): value is PublishedReleaseFact {
61 if (!isRecord(value)) return false;
62 return (
63 typeof value.tag === "string" &&
64 typeof value.version === "string" &&
65 value.tag === `v${value.version}` &&
66 typeof value.publishedAt === "string" &&
67 Number.isFinite(Date.parse(value.publishedAt)) &&
68 value.url === `https://github.com/Hmbown/CodeWhale/releases/tag/${value.tag}`
69 );
70 }
71
72 export function isRepoFacts(value: unknown): value is RepoFacts {
73 if (!isRecord(value)) return false;
74 const sourceRevisionValid =
75 value.sourceRevision === null ||
76 (typeof value.sourceRevision === "string" && /^[0-9a-f]{40}$/i.test(value.sourceRevision));
77 const sourceCommittedAtValid =
78 value.sourceCommittedAt === null ||
79 (typeof value.sourceCommittedAt === "string" &&
80 Number.isFinite(Date.parse(value.sourceCommittedAt)));
81 const sourceProvenancePaired =
82 (value.sourceRevision === null) === (value.sourceCommittedAt === null);
83
84 return (
85 typeof value.generatedAt === "string" &&
86 sourceRevisionValid &&
87 sourceCommittedAtValid &&
88 sourceProvenancePaired &&
89 typeof value.version === "string" &&
90 Array.isArray(value.crates) &&
91 Array.isArray(value.sandboxBackends) &&
92 Array.isArray(value.providers) &&
93 value.providers.every(
94 (provider) =>
95 isRecord(provider) &&
96 typeof provider.id === "string" &&
97 typeof provider.label === "string" &&
98 typeof provider.env === "string",
99 ) &&
100 Array.isArray(value.models) &&
101 value.models.every(isModelFact) &&
102 (value.defaultModel === null || typeof value.defaultModel === "string") &&
103 (value.nodeEngines === null || typeof value.nodeEngines === "string") &&
104 (value.toolCount === null || typeof value.toolCount === "number") &&
105 (value.license === null || typeof value.license === "string") &&
106 (value.latestPublishedRelease === null ||
107 isPublishedRelease(value.latestPublishedRelease))
108 );
109 }
110
111 function newestPublishedRelease(
112 build: PublishedReleaseFact | null,
113 cached: PublishedReleaseFact | null,
114 ): PublishedReleaseFact | null {
115 if (!build) return cached;
116 if (!cached) return build;
117 return Date.parse(cached.publishedAt) > Date.parse(build.publishedAt) ? cached : build;
118 }
119
120 /**
121 * Select the mechanical fact snapshot independently from the latest published
122 * release. A legacy or malformed KV value cannot replace a newer build merely
123 * because it exists. Source commit timestamps provide ordering, while an exact
124 * revision match permits the runtime snapshot for the same source.
125 */
126 export function resolveFacts(build: RepoFacts, cached: unknown): FactsResolution {
127 if (cached == null) {
128 return {
129 facts: build,
130 source: "build",
131 reason: "no-kv-snapshot",
132 buildSourceRevision: build.sourceRevision,
133 kvSourceRevision: null,
134 };
135 }
136 if (!isRepoFacts(cached)) {
137 return {
138 facts: build,
139 source: "build",
140 reason: "invalid-kv-snapshot",
141 buildSourceRevision: build.sourceRevision,
142 kvSourceRevision: null,
143 };
144 }
145
146 const latestPublishedRelease = newestPublishedRelease(
147 build.latestPublishedRelease,
148 cached.latestPublishedRelease,
149 );
150 const withLatestRelease = (facts: RepoFacts): RepoFacts => ({
151 ...facts,
152 latestPublishedRelease,
153 });
154
155 if (!cached.sourceRevision || !cached.sourceCommittedAt) {
156 return {
157 facts: withLatestRelease(build),
158 source: "build",
159 reason: "kv-missing-source-provenance",
160 buildSourceRevision: build.sourceRevision,
161 kvSourceRevision: cached.sourceRevision,
162 };
163 }
164 if (!build.sourceRevision || !build.sourceCommittedAt) {
165 return {
166 facts: withLatestRelease(build),
167 source: "build",
168 reason: "build-missing-source-provenance",
169 buildSourceRevision: build.sourceRevision,
170 kvSourceRevision: cached.sourceRevision,
171 };
172 }
173 if (cached.sourceRevision === build.sourceRevision) {
174 return {
175 facts: withLatestRelease(cached),
176 source: "kv",
177 reason: "same-source-revision",
178 buildSourceRevision: build.sourceRevision,
179 kvSourceRevision: cached.sourceRevision,
180 };
181 }
182
183 const buildTime = Date.parse(build.sourceCommittedAt);
184 const cachedTime = Date.parse(cached.sourceCommittedAt);
185 if (Number.isFinite(buildTime) && Number.isFinite(cachedTime) && cachedTime > buildTime) {
186 return {
187 facts: withLatestRelease(cached),
188 source: "kv",
189 reason: "kv-source-newer",
190 buildSourceRevision: build.sourceRevision,
191 kvSourceRevision: cached.sourceRevision,
192 };
193 }
194
195 return {
196 facts: withLatestRelease(build),
197 source: "build",
198 reason: "build-source-newer-or-ambiguous",
199 buildSourceRevision: build.sourceRevision,
200 kvSourceRevision: cached.sourceRevision,
201 };
202 }
203
204 async function getKv(): Promise<KVNamespace | undefined> {
205 if (process.env.NEXT_PHASE === "phase-production-build") {
206 return undefined;
207 }
208
209 try {
210 const mod = await import("@opennextjs/cloudflare");
211 const ctx = await mod.getCloudflareContext({ async: true });
212 return (ctx.env as { CURATED_KV?: KVNamespace }).CURATED_KV;
213 } catch {
214 return undefined;
215 }
216 }
217
218 export async function getFactsWithProvenance(): Promise<FactsResolution> {
219 try {
220 const kv = await getKv();
221 if (!kv) return resolveFacts(BUILD_FACTS, null);
222 const raw = await kv.get(KV_KEY);
223 if (!raw) return resolveFacts(BUILD_FACTS, null);
224 return resolveFacts(BUILD_FACTS, JSON.parse(raw) as unknown);
225 } catch {
226 return resolveFacts(BUILD_FACTS, null);
227 }
228 }
229
230 /** Resolved facts for the current request, with fail-safe build precedence. */
231 export async function getFacts(): Promise<RepoFacts> {
232 return (await getFactsWithProvenance()).facts;
233 }
234
234 lines TYPESCRIPT