Skip to content
File

Blob: .opencode/tools/compat-date-at.ts

typescript233 lines
1import { tool } from '@opencode-ai/plugin';
2import path from 'path';
3import { runCapnpcCapnp, resolveCapnpPath } from './capnp.ts';
4 
5export default tool({
6 description:
7 'List which compatibility flags are active or inactive at a given date. ' +
8 'Parses compatibility-date.capnp via the capnp compiler. Useful for understanding ' +
9 'what behavior a worker gets at a specific compat date, debugging test variant ' +
10 'failures (@, @all-compat-flags, @all-autogates), and writing .wd-test configs.',
11 args: {
12 date: tool.schema
13 .string()
14 .optional()
15 .describe(
16 'Compatibility date (YYYY-MM-DD). Omit to list all flags with their enable dates.'
17 ),
18 flag: tool.schema
19 .string()
20 .optional()
21 .describe(
22 'Filter by flag name substring (matches enable flag, disable flag, or field name)'
23 ),
24 },
25 async execute(args, ctx) {
26 const root = ctx.worktree || ctx.directory;
27 const flags = await parseCompatFlags(root);
28 if (typeof flags === 'string') return flags;
29 
30 let filtered = flags;
31 
32 if (args.flag) {
33 const q = args.flag.toLowerCase();
34 filtered = flags.filter(
35 (f) =>
36 f.fieldName.toLowerCase().includes(q) ||
37 f.enableFlag?.toLowerCase().includes(q) ||
38 f.disableFlag?.toLowerCase().includes(q)
39 );
40 if (filtered.length === 0) {
41 return `No flags matching '${args.flag}'. There are ${flags.length} total flags.`;
42 }
43 }
44 
45 if (!args.date) {
46 return formatAllFlags(filtered);
47 }
48 
49 if (!/^\d{4}-\d{2}-\d{2}$/.test(args.date)) {
50 return `Invalid date format: '${args.date}'. Use YYYY-MM-DD.`;
51 }
52 
53 return formatAtDate(filtered, args.date);
54 },
55});
56 
57// --- Types ---
58 
59interface CompatFlag {
60 fieldName: string;
61 ordinal: number;
62 enableFlag: string | null;
63 disableFlag: string | null;
64 enableDate: string | null;
65 enableAllDates: boolean;
66 experimental: boolean;
67 neededByFl: boolean;
68 impliedBy: string | null;
69 obsolete: boolean;
70}
71 
72// --- Parsing ---
73 
74const COMPAT_CAPNP = 'src/workerd/io/compatibility-date.capnp';
75 
76async function parseCompatFlags(root: string): Promise<CompatFlag[] | string> {
77 const filePath = resolveCapnpPath(root, COMPAT_CAPNP);
78 const result = await runCapnpcCapnp(root, filePath);
79 if (typeof result !== 'string') return result.error;
80 
81 const flags: CompatFlag[] = [];
82 
83 for (const line of result.split('\n')) {
84 const m = line.match(/^\s+(\w+)\s+@(\d+)\s+:Bool/);
85 if (!m) continue;
86 
87 const fieldName = m[1];
88 const ordinal = parseInt(m[2], 10);
89 
90 flags.push({
91 fieldName,
92 ordinal,
93 enableFlag: extractAnno(line, 'compatEnableFlag'),
94 disableFlag: extractAnno(line, 'compatDisableFlag'),
95 enableDate: extractAnno(line, 'compatEnableDate'),
96 enableAllDates: line.includes('$compatEnableAllDates('),
97 experimental: line.includes('$experimental('),
98 neededByFl: line.includes('$neededByFl('),
99 impliedBy: extractRawAnno(line, 'impliedByAfterDate'),
100 obsolete: fieldName.startsWith('obsolete'),
101 });
102 }
103 
104 return flags;
105}
106 
107function extractAnno(line: string, name: string): string | null {
108 const m = line.match(new RegExp(`\\$${name}\\("([^"]*)"\\)`));
109 return m ? m[1] : null;
110}
111 
112function extractRawAnno(line: string, name: string): string | null {
113 const m = line.match(new RegExp(`\\$${name}\\(([^)]+)\\)`));
114 return m ? m[1] : null;
115}
116 
117// --- Formatting ---
118 
119function formatAllFlags(flags: CompatFlag[]): string {
120 const active = flags.filter((f) => !f.obsolete);
121 const obsolete = flags.filter((f) => f.obsolete);
122 
123 const dated = active
124 .filter((f) => f.enableDate || f.enableAllDates)
125 .sort((a, b) => (a.enableDate || '').localeCompare(b.enableDate || ''));
126 const experimental = active.filter(
127 (f) => f.experimental && !f.enableDate && !f.enableAllDates
128 );
129 const noDate = active.filter(
130 (f) => !f.experimental && !f.enableDate && !f.enableAllDates
131 );
132 
133 let out = `## All Compatibility Flags (${active.length} active, ${obsolete.length} obsolete)\n\n`;
134 
135 if (dated.length > 0) {
136 out += `### Date-gated (${dated.length})\n\n`;
137 for (const f of dated) {
138 const d = f.enableAllDates ? 'ALL DATES' : f.enableDate;
139 const extra: string[] = [];
140 if (f.neededByFl) extra.push('FL');
141 if (f.experimental) extra.push('experimental');
142 if (f.impliedBy) extra.push(`implied by ${f.impliedBy}`);
143 const suffix = extra.length > 0 ? ` (${extra.join(', ')})` : '';
144 out += `- **${d}**: \`${f.enableFlag || f.fieldName}\`${suffix}\n`;
145 }
146 }
147 
148 if (noDate.length > 0) {
149 out += `\n### Opt-in only โ€” no default date (${noDate.length})\n\n`;
150 for (const f of noDate) {
151 const extra: string[] = [];
152 if (f.impliedBy) extra.push(`implied by ${f.impliedBy}`);
153 const suffix = extra.length > 0 ? ` (${extra.join(', ')})` : '';
154 out += `- \`${f.enableFlag || f.fieldName}\`${suffix}\n`;
155 }
156 }
157 
158 if (experimental.length > 0) {
159 out += `\n### Experimental โ€” requires --experimental (${experimental.length})\n\n`;
160 for (const f of experimental) {
161 out += `- \`${f.enableFlag || f.fieldName}\`\n`;
162 }
163 }
164 
165 return out;
166}
167 
168function formatAtDate(flags: CompatFlag[], date: string): string {
169 const active = flags.filter((f) => !f.obsolete);
170 
171 const enabled: CompatFlag[] = [];
172 const notYetEnabled: CompatFlag[] = [];
173 const optInOnly: CompatFlag[] = [];
174 const experimental: CompatFlag[] = [];
175 
176 for (const f of active) {
177 if (f.enableAllDates) {
178 enabled.push(f);
179 } else if (f.enableDate) {
180 if (f.enableDate <= date) {
181 enabled.push(f);
182 } else {
183 notYetEnabled.push(f);
184 }
185 } else if (f.experimental) {
186 experimental.push(f);
187 } else {
188 optInOnly.push(f);
189 }
190 }
191 
192 let out = `## Flags at compatibility date "${date}"\n\n`;
193 out += `- **${enabled.length}** enabled by default\n`;
194 out += `- **${notYetEnabled.length}** not yet enabled (future date)\n`;
195 out += `- **${optInOnly.length}** opt-in only (no date set)\n`;
196 out += `- **${experimental.length}** experimental\n\n`;
197 
198 if (enabled.length > 0) {
199 out += `### Enabled by default (${enabled.length})\n\n`;
200 for (const f of enabled.sort((a, b) =>
201 (a.enableDate || '').localeCompare(b.enableDate || '')
202 )) {
203 const d = f.enableAllDates ? 'all' : f.enableDate;
204 out += `- \`${f.enableFlag || f.fieldName}\` (since ${d})\n`;
205 }
206 }
207 
208 if (notYetEnabled.length > 0) {
209 out += `\n### Not yet enabled โ€” date is after "${date}" (${notYetEnabled.length})\n\n`;
210 for (const f of notYetEnabled.sort((a, b) =>
211 (a.enableDate || '').localeCompare(b.enableDate || '')
212 )) {
213 out += `- \`${f.enableFlag || f.fieldName}\` (${f.enableDate})\n`;
214 }
215 }
216 
217 if (optInOnly.length > 0) {
218 out += `\n### Opt-in only (${optInOnly.length})\n\n`;
219 for (const f of optInOnly) {
220 out += `- \`${f.enableFlag || f.fieldName}\`\n`;
221 }
222 }
223 
224 if (experimental.length > 0) {
225 out += `\n### Experimental (${experimental.length})\n\n`;
226 for (const f of experimental) {
227 out += `- \`${f.enableFlag || f.fieldName}\`\n`;
228 }
229 }
230 
231 return out;
232}