@boyingliu01/xp-gate 0.35.23 → 0.36.0
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/adapter-common.sh +78 -36
- package/adapters/csharp.sh +245 -0
- package/adapters/gate-3.sh +1 -1
- package/adapters/gate-4.sh +1 -1
- package/adapters/gate-9.sh +37 -2
- package/adapters/go.sh +6 -11
- package/adapters/iac.sh +45 -5
- package/adapters/python.sh +6 -11
- package/bin/xp-gate.js +18 -11
- package/gate-10.sh +18 -3
- package/gate-13-compiler-strictness.sh +181 -0
- package/gate-3.sh +1 -1
- package/gate-4.sh +1 -1
- package/gate-9.sh +37 -2
- package/gates/common.ts +9 -2
- package/hooks/adapter-common.sh +78 -36
- package/hooks/gate-10.sh +18 -3
- package/hooks/gate-13-compiler-strictness.sh +181 -0
- package/hooks/gate-3.sh +1 -1
- package/hooks/gate-4.sh +1 -1
- package/hooks/gate-9.sh +37 -2
- package/hooks/gate-registry.sh +161 -0
- package/hooks/lib/test-failure.sh +29 -4
- package/hooks/pre-commit +122 -3
- package/lib/__tests__/agent-runtime-journal.test.js +42 -18
- package/lib/__tests__/check-single-file.test.js +55 -4
- package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
- package/lib/__tests__/doctor-delphi.test.js +178 -3
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +3 -3
- package/lib/__tests__/install-skill-full-dir.test.js +45 -2
- package/lib/__tests__/install-skill.test.js +191 -23
- package/lib/__tests__/mirror-sync.test.js +23 -5
- package/lib/__tests__/uninstall-global-residue.test.js +394 -9
- package/lib/__tests__/update-skill-flags.test.js +78 -0
- package/lib/__tests__/update-skill.test.js +334 -57
- package/lib/detect-deps.js +24 -0
- package/lib/doctor.js +80 -12
- package/lib/install-skill.js +85 -21
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/uninstall.js +143 -12
- package/lib/update-skill.js +239 -28
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/lib/index.d.ts.map +1 -1
- package/plugins/dsh/lib/index.js +7 -4
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/index.test.ts +25 -0
- package/plugins/dsh/src/index.ts +7 -4
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/principles/adapters/csharp.ts +309 -0
- package/principles/analyzer.ts +5 -0
- package/scripts/delphi-external-review.cjs +130 -101
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-specification-alignment/AGENTS.md +4 -4
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { BaseAdapter } from './base';
|
|
2
|
+
import { Adapter } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lexical state for the C# block scanner: which comment/quote the scan is
|
|
6
|
+
* currently inside. Braces seen in any of these states are data, not structure.
|
|
7
|
+
*
|
|
8
|
+
* C# needs one more state than C++: verbatim strings (`@"..."`) terminate on a
|
|
9
|
+
* doubled quote rather than a backslash escape, and raw string literals
|
|
10
|
+
* (`"""..."""`) have no escape character at all. Treating either as a normal
|
|
11
|
+
* literal would mis-judge where the body ends.
|
|
12
|
+
*/
|
|
13
|
+
interface CSharpLexState {
|
|
14
|
+
inString: boolean;
|
|
15
|
+
/** Which quote character opened the string (`'` or `"`). */
|
|
16
|
+
stringChar: string;
|
|
17
|
+
inBlockComment: boolean;
|
|
18
|
+
/** `@"..."`: `""` is an escaped quote, backslash is literal. */
|
|
19
|
+
inVerbatimString: boolean;
|
|
20
|
+
/** `"""..."""`: delimited by a run of >= 3 quotes. */
|
|
21
|
+
inRawString: boolean;
|
|
22
|
+
rawStringQuoteCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* C# language adapter for the principles checker (Gate 4).
|
|
27
|
+
*
|
|
28
|
+
* Why this file exists: `analyzer.ts`'s `adapterMap` had no `.cs` entry, so
|
|
29
|
+
* `getAdapterForFile('A.cs')` returned `null`. `gate-4.sh` DID pass `.cs` paths
|
|
30
|
+
* to the checker (its extension filter includes `cs`), so the file was accepted
|
|
31
|
+
* and then dropped — a gate that printed "checking" and checked nothing. The
|
|
32
|
+
* AC that claimed Gate 4 covered C# by testing the *filter* alone was therefore
|
|
33
|
+
* wrong, which is what this adapter and its tests fix.
|
|
34
|
+
*
|
|
35
|
+
* Regex-based rather than AST-based, following `cpp.ts`: no C# parser is
|
|
36
|
+
* available in this zero-dependency package, and the same lexical-blindness
|
|
37
|
+
* problem (braces inside literals/comments) applies verbatim.
|
|
38
|
+
*/
|
|
39
|
+
export class CSharpAdapter extends BaseAdapter implements Adapter {
|
|
40
|
+
detectLanguage(): string {
|
|
41
|
+
const lower = this.filePath.toLowerCase();
|
|
42
|
+
if (lower.endsWith('.cs') || lower.endsWith('.csx')) {
|
|
43
|
+
return 'csharp';
|
|
44
|
+
}
|
|
45
|
+
return super.detectLanguage();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parseAST(): unknown {
|
|
49
|
+
return this.createParseResult('csharp');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
extractFunctions(): unknown[] {
|
|
53
|
+
const matches: unknown[] = [];
|
|
54
|
+
|
|
55
|
+
// C# method declaration: optional modifiers, return type, name, params,
|
|
56
|
+
// then either a block `{`, an expression body `=>`, or `;` (abstract /
|
|
57
|
+
// interface members). `async` and generics (`Task<int>`) must be tolerated
|
|
58
|
+
// or every async method in a modern codebase is invisible.
|
|
59
|
+
const methodRegex =
|
|
60
|
+
/(?:^|\n)[ \t]*(?:(?:public|private|protected|internal|static|async|override|virtual|sealed|abstract|extern|unsafe|new|partial)\s+)*(?:[\w.<>\[\],?]+\s+)?([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*\(([^)]*)\)\s*(?:where\s[^{;]*)?(?:=>|\{|;)/g;
|
|
61
|
+
|
|
62
|
+
let match: RegExpExecArray | null;
|
|
63
|
+
while ((match = methodRegex.exec(this.fileContent)) !== null) {
|
|
64
|
+
const name = match[1];
|
|
65
|
+
// Constructors and `new` expressions look like the pattern above; the
|
|
66
|
+
// reserved words are not method names as far as the rules care.
|
|
67
|
+
if (CSharpAdapter.RESERVED.has(name)) continue;
|
|
68
|
+
|
|
69
|
+
const params = match[2].trim();
|
|
70
|
+
const paramCount = params === '' ? 0 : params.split(',').length;
|
|
71
|
+
|
|
72
|
+
matches.push(this.createCodeMatch(name, 'function', match.index));
|
|
73
|
+
const fnMatch = matches[matches.length - 1];
|
|
74
|
+
this.annotate(fnMatch, 'paramCount', paramCount);
|
|
75
|
+
this.annotateBodyMetrics(fnMatch);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Constructors: `public Foo(...)` has no return type, so the regex above
|
|
79
|
+
// can miss it when the modifier list is empty or minimal.
|
|
80
|
+
const ctorRegex = /(?:^|\n)[ \t]*(?:(?:public|private|protected|internal|static)\s+)*([A-Za-z_]\w*)\s*\(([^)]*)\)\s*(?::\s*(?:base|this)\s*\([^)]*\)\s*)?\{/g;
|
|
81
|
+
while ((match = ctorRegex.exec(this.fileContent)) !== null) {
|
|
82
|
+
const name = match[1];
|
|
83
|
+
if (CSharpAdapter.RESERVED.has(name)) continue;
|
|
84
|
+
const params = match[2].trim();
|
|
85
|
+
const paramCount = params === '' ? 0 : params.split(',').length;
|
|
86
|
+
matches.push(this.createCodeMatch(name, 'constructor', match.index));
|
|
87
|
+
const ctorMatch = matches[matches.length - 1];
|
|
88
|
+
this.annotate(ctorMatch, 'paramCount', paramCount);
|
|
89
|
+
this.annotateBodyMetrics(ctorMatch);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return matches;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
extractClasses(): unknown[] {
|
|
96
|
+
const matches: unknown[] = [];
|
|
97
|
+
// The trailing group accepts BOTH shapes a type declaration takes: a body
|
|
98
|
+
// (`{`) and a body-less primary-constructor record (`record Delta(int X);`,
|
|
99
|
+
// C# 9+). Requiring `{` made every such record invisible to the god-class /
|
|
100
|
+
// large-file rules.
|
|
101
|
+
const classRegex =
|
|
102
|
+
/\b(?:(?:public|private|protected|internal|static|sealed|abstract|partial|readonly|unsafe|new)\s+)*(class|interface|struct|record|enum)\s+([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*(?:\([^)]*\))?\s*(?::\s*[^{\n;]+)?\s*(?:\{|;)/g;
|
|
103
|
+
|
|
104
|
+
let match: RegExpExecArray | null;
|
|
105
|
+
while ((match = classRegex.exec(this.fileContent)) !== null) {
|
|
106
|
+
matches.push(this.createCodeMatch(match[2], match[1], match.index));
|
|
107
|
+
const body = this.extractCodeBlock(match.index);
|
|
108
|
+
// methodCount feeds the god-class rule; counting declarations in the body
|
|
109
|
+
// is the only signal available without a real parser.
|
|
110
|
+
const methodCount = (body.match(/\b(?:public|private|protected|internal)\s+[^;{}]*\([^)]*\)\s*(?:=>|\{)/g) || []).length;
|
|
111
|
+
this.annotate(matches[matches.length - 1], 'methodCount', methodCount);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return matches;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Attach a computed metric to a match returned by `createCodeMatch`.
|
|
119
|
+
*
|
|
120
|
+
* The base helper builds `{name, type, line, code}`; the 14 rules read extra
|
|
121
|
+
* fields (length / paramCount / nestingDepth / methodCount / hasTryCatch), and
|
|
122
|
+
* a missing field is why 12 of those rules were structurally unable to fire on
|
|
123
|
+
* TypeScript before #446. This keeps the same lesson from repeating here.
|
|
124
|
+
*/
|
|
125
|
+
private annotate(target: unknown, key: string, value: number): void {
|
|
126
|
+
(target as Record<string, unknown>)[key] = value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compute the body-derived metrics the rules read, from this match's own
|
|
131
|
+
* `code` (which `createCodeMatch` already cut with the literal-aware scanner).
|
|
132
|
+
*
|
|
133
|
+
* length / nestingDepth / hasTryCatch all come from the body, so computing
|
|
134
|
+
* them anywhere else would re-implement the brace scan and drift from it.
|
|
135
|
+
*/
|
|
136
|
+
private annotateBodyMetrics(target: unknown): void {
|
|
137
|
+
const m = target as Record<string, unknown>;
|
|
138
|
+
const body = typeof m.code === 'string' ? m.code : '';
|
|
139
|
+
|
|
140
|
+
// Lines of the body, minus the declaration line itself.
|
|
141
|
+
const lineCount = body === '' ? 0 : body.split(/\r?\n/).length;
|
|
142
|
+
m.length = lineCount;
|
|
143
|
+
|
|
144
|
+
// Nesting depth = the deepest run of `{` that the scanner saw. Literals and
|
|
145
|
+
// comments were already removed by the scanner before this string was cut.
|
|
146
|
+
let depth = 0;
|
|
147
|
+
let maxDepth = 0;
|
|
148
|
+
for (const ch of body) {
|
|
149
|
+
if (ch === '{') { depth++; if (depth > maxDepth) maxDepth = depth; }
|
|
150
|
+
else if (ch === '}') depth--;
|
|
151
|
+
}
|
|
152
|
+
// A method body contributes its own level, so a flat body reads 1.
|
|
153
|
+
m.nestingDepth = maxDepth > 0 ? maxDepth : 0;
|
|
154
|
+
|
|
155
|
+
m.hasTryCatch = /\btry\s*\{/.test(body) && /\bcatch\s*[({]/.test(body);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Words that the C# grammar allows where a method name appears. */
|
|
159
|
+
private static readonly RESERVED = new Set([
|
|
160
|
+
'if', 'for', 'foreach', 'while', 'switch', 'catch', 'using', 'lock', 'return',
|
|
161
|
+
'new', 'throw', 'case', 'do', 'else', 'fixed', 'try', 'finally', 'yield',
|
|
162
|
+
'await', 'typeof', 'sizeof', 'nameof', 'default', 'checked', 'unchecked',
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
override extractCodeBlock(startPos: number, maxFallback: number = 200): string {
|
|
166
|
+
const state = { inBlock: false, braceCount: 0 };
|
|
167
|
+
const lex: CSharpLexState = {
|
|
168
|
+
inString: false,
|
|
169
|
+
stringChar: '',
|
|
170
|
+
inBlockComment: false,
|
|
171
|
+
inVerbatimString: false,
|
|
172
|
+
inRawString: false,
|
|
173
|
+
rawStringQuoteCount: 0,
|
|
174
|
+
};
|
|
175
|
+
let endPos = startPos;
|
|
176
|
+
|
|
177
|
+
for (let i = startPos; i < this.fileContent.length;) {
|
|
178
|
+
const skip = this.lexSkip(i, lex);
|
|
179
|
+
if (skip > 0) { i += skip; continue; }
|
|
180
|
+
|
|
181
|
+
const result = this.advanceBraceTracker(this.fileContent[i], i, state);
|
|
182
|
+
if (result.closed) {
|
|
183
|
+
endPos = result.endPos;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (endPos > startPos) {
|
|
190
|
+
return this.fileContent.substring(startPos, endPos);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return this.fallbackCodeBlock(startPos, maxFallback);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Report how far the lexer can skip from `i`, updating `lex` as it goes.
|
|
198
|
+
*
|
|
199
|
+
* Same contract as `cpp.ts`'s `lexSkip`: returning 0 means the caller must
|
|
200
|
+
* inspect the character itself, which is what lets a `{` both end a
|
|
201
|
+
* literal-free run and open the block.
|
|
202
|
+
*/
|
|
203
|
+
private lexSkip(i: number, lex: CSharpLexState): number {
|
|
204
|
+
const char = this.fileContent[i];
|
|
205
|
+
const nextChar = this.fileContent[i + 1] || '';
|
|
206
|
+
|
|
207
|
+
if (lex.inRawString) return this.skipRawStringBody(i, lex);
|
|
208
|
+
if (lex.inBlockComment) return this.skipBlockCommentBody(i, char, nextChar, lex);
|
|
209
|
+
if (lex.inString) return this.skipStringBody(i, char, lex);
|
|
210
|
+
|
|
211
|
+
if (char === '/' && nextChar === '/') return this.skipLineComment(i);
|
|
212
|
+
if (char === '/' && nextChar === '*') {
|
|
213
|
+
lex.inBlockComment = true;
|
|
214
|
+
return 2;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// `@"` opens a verbatim string; `"` alone opens a normal one.
|
|
218
|
+
if (char === '@' && nextChar === '"') {
|
|
219
|
+
lex.inString = true;
|
|
220
|
+
lex.inVerbatimString = true;
|
|
221
|
+
lex.stringChar = '"';
|
|
222
|
+
return 2;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// A run of 3+ quotes is a raw string literal.
|
|
226
|
+
if (char === '"') {
|
|
227
|
+
let run = 0;
|
|
228
|
+
let k = i;
|
|
229
|
+
while (this.fileContent[k] === '"') { run++; k++; }
|
|
230
|
+
if (run >= 3) {
|
|
231
|
+
lex.inRawString = true;
|
|
232
|
+
lex.rawStringQuoteCount = run;
|
|
233
|
+
return run;
|
|
234
|
+
}
|
|
235
|
+
lex.inString = true;
|
|
236
|
+
lex.inVerbatimString = false;
|
|
237
|
+
lex.stringChar = '"';
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (char === "'") {
|
|
242
|
+
lex.inString = true;
|
|
243
|
+
lex.inVerbatimString = false;
|
|
244
|
+
lex.stringChar = "'";
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Skip a `//` comment through, but not including, the newline. */
|
|
252
|
+
private skipLineComment(start: number): number {
|
|
253
|
+
let i = start;
|
|
254
|
+
while (i < this.fileContent.length && this.fileContent[i] !== '\n') i++;
|
|
255
|
+
return i - start;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private skipBlockCommentBody(
|
|
259
|
+
i: number,
|
|
260
|
+
char: string,
|
|
261
|
+
nextChar: string,
|
|
262
|
+
lex: CSharpLexState,
|
|
263
|
+
): number {
|
|
264
|
+
if (char === '*' && nextChar === '/') {
|
|
265
|
+
lex.inBlockComment = false;
|
|
266
|
+
return 2;
|
|
267
|
+
}
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private skipStringBody(i: number, char: string, lex: CSharpLexState): number {
|
|
272
|
+
if (char !== lex.stringChar) return 1;
|
|
273
|
+
|
|
274
|
+
// Verbatim strings escape a quote by DOUBLING it: `@"a""b"` is `a"b`.
|
|
275
|
+
if (lex.inVerbatimString) {
|
|
276
|
+
if (this.fileContent[i + 1] === '"') return 2;
|
|
277
|
+
lex.inString = false;
|
|
278
|
+
lex.inVerbatimString = false;
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A quote closes the literal only after an EVEN number of backslashes:
|
|
283
|
+
// `"\\"` is a backslash then a terminator, `"\""` is a quote as data.
|
|
284
|
+
let backslashCount = 0;
|
|
285
|
+
let j = i - 1;
|
|
286
|
+
while (j >= 0 && this.fileContent[j] === '\\') {
|
|
287
|
+
backslashCount++;
|
|
288
|
+
j--;
|
|
289
|
+
}
|
|
290
|
+
if (backslashCount % 2 === 0) lex.inString = false;
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** A raw string closes on a quote run of the same length that opened it. */
|
|
295
|
+
private skipRawStringBody(i: number, lex: CSharpLexState): number {
|
|
296
|
+
if (this.fileContent[i] !== '"') return 1;
|
|
297
|
+
|
|
298
|
+
let run = 0;
|
|
299
|
+
let k = i;
|
|
300
|
+
while (this.fileContent[k] === '"') { run++; k++; }
|
|
301
|
+
|
|
302
|
+
if (run >= lex.rawStringQuoteCount) {
|
|
303
|
+
lex.inRawString = false;
|
|
304
|
+
lex.rawStringQuoteCount = 0;
|
|
305
|
+
return run;
|
|
306
|
+
}
|
|
307
|
+
return run;
|
|
308
|
+
}
|
|
309
|
+
}
|
package/principles/analyzer.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { DartAdapter } from './adapters/dart';
|
|
|
9
9
|
import { SwiftAdapter } from './adapters/swift';
|
|
10
10
|
import { ObjectiveCAdapter } from './adapters/objectivec';
|
|
11
11
|
import { CppAdapter } from './adapters/cpp';
|
|
12
|
+
import { CSharpAdapter } from './adapters/csharp';
|
|
12
13
|
import { extname } from 'path';
|
|
13
14
|
|
|
14
15
|
export type { Violation } from './types';
|
|
@@ -63,6 +64,10 @@ export function getAdapterForFile(filePath: string): Adapter | null {
|
|
|
63
64
|
'.swift': SwiftAdapter,
|
|
64
65
|
'.m': ObjectiveCAdapter,
|
|
65
66
|
'.mm': ObjectiveCAdapter,
|
|
67
|
+
// #717: without this entry `getAdapterForFile('A.cs')` returned null while
|
|
68
|
+
// gate-4.sh had already accepted the file, so Gate 4 inspected no C# at all.
|
|
69
|
+
'.cs': CSharpAdapter,
|
|
70
|
+
'.csx': CSharpAdapter,
|
|
66
71
|
'.cpp': CppAdapter,
|
|
67
72
|
'.cxx': CppAdapter,
|
|
68
73
|
'.cc': CppAdapter,
|
|
@@ -659,10 +659,129 @@ async function callExpertWithSchemaRetry({ provider, model, systemPrompt, userPr
|
|
|
659
659
|
}
|
|
660
660
|
|
|
661
661
|
// ── API call ───────────────────────────────────────────────────────────
|
|
662
|
-
async function callModelAPI(providerConfig, model, systemPrompt, userPrompt, options = {}) {
|
|
663
|
-
const url = `${providerConfig.base_url.replace(/\/$/, '')}/chat/completions`;
|
|
664
662
|
|
|
665
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Map a 429 to a verdict. Not every 429 is a transient rate limit — WhaleCloud
|
|
665
|
+
* returns `type: "budget_exceeded"` in two distinct non-retryable cases, and a
|
|
666
|
+
* blind retry cannot clear either and would burn two round-trips before failing:
|
|
667
|
+
* - `error.param === "category_quota"`: the account has no ECONOMY/STANDARD
|
|
668
|
+
* quota configured for the model ("需要专项额度...尚未配置") — needs an admin.
|
|
669
|
+
* - `error.param === "cost_quota"`: the account's daily spending cap is
|
|
670
|
+
* exhausted ("今日额度已耗尽...限额: 100.00元") — resets the next day.
|
|
671
|
+
* Surface which kind it is so the caller knows whether to wait or to ask for an
|
|
672
|
+
* admin change, rather than treating both as a generic rate limit.
|
|
673
|
+
*
|
|
674
|
+
* Extracted from `callModelAPI` so the branch reads as one rule (#722 follow-up:
|
|
675
|
+
* the enclosing function was CCN 33 against a block threshold of 10).
|
|
676
|
+
*/
|
|
677
|
+
async function classifyRateLimitResponse(response) {
|
|
678
|
+
let quotaType = '';
|
|
679
|
+
try {
|
|
680
|
+
const body = await response.json();
|
|
681
|
+
if (body && body.error && body.error.type === 'budget_exceeded') {
|
|
682
|
+
quotaType = body.error.param === 'cost_quota' ? 'daily budget exhausted' : 'quota not configured';
|
|
683
|
+
}
|
|
684
|
+
} catch {
|
|
685
|
+
// body is not JSON; fall through to the generic rate-limit handling
|
|
686
|
+
}
|
|
687
|
+
if (quotaType) {
|
|
688
|
+
return { error: true, retryable: false, message: `WhaleCloud budget_exceeded: ${quotaType} for this model.` };
|
|
689
|
+
}
|
|
690
|
+
return { error: true, retryable: true, message: 'Rate limit exceeded (429).' };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Map a 5xx to a verdict, reading the provider's structured error body the same
|
|
695
|
+
* way the 429 branch does instead of discarding it (#705).
|
|
696
|
+
*
|
|
697
|
+
* Measured on the whalecloud gateway: a prompt containing
|
|
698
|
+
* `new ProcessBuilder(args).start()` makes the gateway fail in its
|
|
699
|
+
* response-processing stage and return 500 with
|
|
700
|
+
* {"error":{"type":"response_processing_error",
|
|
701
|
+
* "param":"non_stream_chat_response","code":"500"}}
|
|
702
|
+
* The old code threw that away and reported "Server error (500).", which reads
|
|
703
|
+
* as a transient outage. `type`/`param` are what tell an operator apart gateway
|
|
704
|
+
* flapping vs. upstream timeout vs. content-triggered processing failure —
|
|
705
|
+
* three problems with three different responses.
|
|
706
|
+
*
|
|
707
|
+
* Retryability is the other half: a content-dependent failure cannot clear by
|
|
708
|
+
* retrying (the content did not change), so marking it retryable burns the
|
|
709
|
+
* orchestrator's retry budget on a guaranteed repeat. Only an explicitly
|
|
710
|
+
* recognised response_processing_error is demoted; every other 5xx keeps the
|
|
711
|
+
* previous retryable=true, and an unparseable body keeps the previous behaviour
|
|
712
|
+
* entirely — reading the body must not change the verdict on its own.
|
|
713
|
+
*/
|
|
714
|
+
async function classifyServerErrorResponse(response) {
|
|
715
|
+
let detail = '';
|
|
716
|
+
let processingError = false;
|
|
717
|
+
try {
|
|
718
|
+
const body = await response.json();
|
|
719
|
+
if (body && body.error && typeof body.error === 'object') {
|
|
720
|
+
const type = typeof body.error.type === 'string' ? body.error.type : '';
|
|
721
|
+
const param = typeof body.error.param === 'string' ? body.error.param : '';
|
|
722
|
+
if (type === 'response_processing_error') {
|
|
723
|
+
processingError = true;
|
|
724
|
+
}
|
|
725
|
+
const parts = [type, param].filter(Boolean);
|
|
726
|
+
if (parts.length > 0) {
|
|
727
|
+
detail = ` Provider reported: ${parts.join('/')}.`;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
} catch {
|
|
731
|
+
// body is not JSON; keep the previous behaviour untouched
|
|
732
|
+
}
|
|
733
|
+
if (processingError) {
|
|
734
|
+
return {
|
|
735
|
+
error: true,
|
|
736
|
+
retryable: false,
|
|
737
|
+
message: `Server error (${response.status}).${detail} This is a content-dependent upstream processing failure, so retrying the same prompt will fail the same way.`,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
return { error: true, retryable: true, message: `Server error (${response.status}).${detail}` };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Map a 2xx response body to a verdict.
|
|
745
|
+
*
|
|
746
|
+
* The gateway must echo which model actually served the request. Writing `null`
|
|
747
|
+
* here used to be tolerated downstream, which let a run that never resolved a
|
|
748
|
+
* model count as a successful expert call (#423). Fail at the source instead,
|
|
749
|
+
* so the evidence never records an unverifiable expert.
|
|
750
|
+
*/
|
|
751
|
+
async function readSuccessfulResponse(response) {
|
|
752
|
+
const data = await response.json();
|
|
753
|
+
if (!isPlainObject(data) || !Array.isArray(data.choices)) {
|
|
754
|
+
return { error: true, message: 'Invalid response from model.' };
|
|
755
|
+
}
|
|
756
|
+
const firstChoice = data.choices[0];
|
|
757
|
+
const message = isPlainObject(firstChoice) ? firstChoice.message : null;
|
|
758
|
+
const content = isPlainObject(message) ? message.content : null;
|
|
759
|
+
if (typeof content !== 'string' || content.trim() === '') {
|
|
760
|
+
return { error: true, message: 'Invalid response from model.' };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const resolvedModel = typeof data.model === 'string' ? data.model.trim() : '';
|
|
764
|
+
if (resolvedModel === '') {
|
|
765
|
+
return {
|
|
766
|
+
error: true,
|
|
767
|
+
message: 'Gateway response did not include a resolved model id; cannot prove which model ran.',
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
success: true,
|
|
773
|
+
content: content.trim(),
|
|
774
|
+
resolved_model: resolvedModel,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Build the request body. `temperature: 0.3` is a Delphi-review choice: low
|
|
780
|
+
* enough to keep verdicts reproducible, non-zero so three experts on the same
|
|
781
|
+
* model are not literally identical runs.
|
|
782
|
+
*/
|
|
783
|
+
function buildChatBody(model, systemPrompt, userPrompt) {
|
|
784
|
+
return {
|
|
666
785
|
model: model,
|
|
667
786
|
messages: [
|
|
668
787
|
{ role: 'system', content: systemPrompt },
|
|
@@ -671,6 +790,11 @@ async function callModelAPI(providerConfig, model, systemPrompt, userPrompt, opt
|
|
|
671
790
|
temperature: 0.3,
|
|
672
791
|
response_format: { type: 'json_object' },
|
|
673
792
|
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function callModelAPI(providerConfig, model, systemPrompt, userPrompt, options = {}) {
|
|
796
|
+
const url = `${providerConfig.base_url.replace(/\/$/, '')}/chat/completions`;
|
|
797
|
+
const body = buildChatBody(model, systemPrompt, userPrompt);
|
|
674
798
|
|
|
675
799
|
// Same constant as the runner default: a second literal here silently
|
|
676
800
|
// diverges whenever the default is retuned (#429).
|
|
@@ -696,113 +820,18 @@ async function callModelAPI(providerConfig, model, systemPrompt, userPrompt, opt
|
|
|
696
820
|
}
|
|
697
821
|
|
|
698
822
|
if (response.status === 429) {
|
|
699
|
-
|
|
700
|
-
// `type: "budget_exceeded"` in two distinct non-retryable cases, which a
|
|
701
|
-
// blind retry cannot clear and would otherwise burn two round-trips before
|
|
702
|
-
// failing:
|
|
703
|
-
// - `error.param === "category_quota"`: the account has no ECONOMY/STANDARD
|
|
704
|
-
// quota configured for the model ("需要专项额度...尚未配置") — needs an
|
|
705
|
-
// admin to add it.
|
|
706
|
-
// - `error.param === "cost_quota"`: the account's daily spending cap is
|
|
707
|
-
// exhausted ("今日额度已耗尽...限额: 100.00元") — resets the next day.
|
|
708
|
-
// Surface which kind it is so the caller knows whether to wait or to request
|
|
709
|
-
// an admin change, rather than treating both as a generic rate limit.
|
|
710
|
-
let quotaType = '';
|
|
711
|
-
try {
|
|
712
|
-
const body = await response.json();
|
|
713
|
-
if (body && body.error && body.error.type === 'budget_exceeded') {
|
|
714
|
-
quotaType = body.error.param === 'cost_quota' ? 'daily budget exhausted' : 'quota not configured';
|
|
715
|
-
}
|
|
716
|
-
} catch {
|
|
717
|
-
// body is not JSON; fall through to the generic rate-limit handling
|
|
718
|
-
}
|
|
719
|
-
if (quotaType) {
|
|
720
|
-
return { error: true, retryable: false, message: `WhaleCloud budget_exceeded: ${quotaType} for this model.` };
|
|
721
|
-
}
|
|
722
|
-
return { error: true, retryable: true, message: 'Rate limit exceeded (429).' };
|
|
823
|
+
return await classifyRateLimitResponse(response);
|
|
723
824
|
}
|
|
724
825
|
|
|
725
826
|
if (response.status >= 500) {
|
|
726
|
-
|
|
727
|
-
// branch above does, instead of discarding it.
|
|
728
|
-
//
|
|
729
|
-
// Measured on the whalecloud gateway: a prompt containing
|
|
730
|
-
// `new ProcessBuilder(args).start()` makes the gateway fail in its
|
|
731
|
-
// response-processing stage and return 500 with
|
|
732
|
-
// {"error":{"type":"response_processing_error",
|
|
733
|
-
// "param":"non_stream_chat_response","code":"500"}}
|
|
734
|
-
// The old code threw that away and reported "Server error (500).", which
|
|
735
|
-
// reads as a transient outage. `type`/`param` are what tell an operator
|
|
736
|
-
// apart gateway flapping vs. upstream timeout vs. content-triggered
|
|
737
|
-
// processing failure — three problems with three different responses.
|
|
738
|
-
//
|
|
739
|
-
// Retryability is the other half: a content-dependent failure cannot clear
|
|
740
|
-
// by retrying (the content did not change), so marking it retryable burns
|
|
741
|
-
// the orchestrator's retry budget on a guaranteed repeat. Only an
|
|
742
|
-
// explicitly recognised response_processing_error is demoted; every other
|
|
743
|
-
// 5xx keeps the previous retryable=true, and an unparseable body keeps the
|
|
744
|
-
// previous behaviour entirely — reading the body must not change the
|
|
745
|
-
// verdict on its own.
|
|
746
|
-
let detail = '';
|
|
747
|
-
let processingError = false;
|
|
748
|
-
try {
|
|
749
|
-
const body = await response.json();
|
|
750
|
-
if (body && body.error && typeof body.error === 'object') {
|
|
751
|
-
const type = typeof body.error.type === 'string' ? body.error.type : '';
|
|
752
|
-
const param = typeof body.error.param === 'string' ? body.error.param : '';
|
|
753
|
-
if (type === 'response_processing_error') {
|
|
754
|
-
processingError = true;
|
|
755
|
-
}
|
|
756
|
-
const parts = [type, param].filter(Boolean);
|
|
757
|
-
if (parts.length > 0) {
|
|
758
|
-
detail = ` Provider reported: ${parts.join('/')}.`;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
} catch {
|
|
762
|
-
// body is not JSON; keep the previous behaviour untouched
|
|
763
|
-
}
|
|
764
|
-
if (processingError) {
|
|
765
|
-
return {
|
|
766
|
-
error: true,
|
|
767
|
-
retryable: false,
|
|
768
|
-
message: `Server error (${response.status}).${detail} This is a content-dependent upstream processing failure, so retrying the same prompt will fail the same way.`,
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
return { error: true, retryable: true, message: `Server error (${response.status}).${detail}` };
|
|
827
|
+
return await classifyServerErrorResponse(response);
|
|
772
828
|
}
|
|
773
829
|
|
|
774
830
|
if (!response.ok) {
|
|
775
831
|
return { error: true, message: `API request failed (${response.status}). Provider response was not included.` };
|
|
776
832
|
}
|
|
777
833
|
|
|
778
|
-
|
|
779
|
-
if (!isPlainObject(data) || !Array.isArray(data.choices)) {
|
|
780
|
-
return { error: true, message: 'Invalid response from model.' };
|
|
781
|
-
}
|
|
782
|
-
const firstChoice = data.choices[0];
|
|
783
|
-
const message = isPlainObject(firstChoice) ? firstChoice.message : null;
|
|
784
|
-
const content = isPlainObject(message) ? message.content : null;
|
|
785
|
-
if (typeof content !== 'string' || content.trim() === '') {
|
|
786
|
-
return { error: true, message: 'Invalid response from model.' };
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// The gateway must echo which model actually served the request. Writing
|
|
790
|
-
// `null` here used to be tolerated downstream, which let a run that never
|
|
791
|
-
// resolved a model count as a successful expert call (#423). Fail at the
|
|
792
|
-
// source instead, so the evidence never records an unverifiable expert.
|
|
793
|
-
const resolvedModel = typeof data.model === 'string' ? data.model.trim() : '';
|
|
794
|
-
if (resolvedModel === '') {
|
|
795
|
-
return {
|
|
796
|
-
error: true,
|
|
797
|
-
message: 'Gateway response did not include a resolved model id; cannot prove which model ran.',
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
return {
|
|
802
|
-
success: true,
|
|
803
|
-
content: content.trim(),
|
|
804
|
-
resolved_model: resolvedModel,
|
|
805
|
-
};
|
|
834
|
+
return await readSuccessfulResponse(response);
|
|
806
835
|
} catch (err) {
|
|
807
836
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
808
837
|
return { error: true, retryable: true, message: `Request timed out (${timeoutMs}ms). Raise --timeout-ms or the provider's timeout_ms.` };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-11-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.
|
|
3
|
+
**Generated:** 2026-11-04
|
|
4
|
+
**Commit:** eb2097b0
|
|
5
|
+
**Branch:** sprint/2026-11-03-02
|
|
6
|
+
**Version:** 0.36.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-11-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.
|
|
3
|
+
**Generated:** 2026-11-04
|
|
4
|
+
**Commit:** eb2097b0
|
|
5
|
+
**Branch:** sprint/2026-11-03-02
|
|
6
|
+
**Version:** 0.36.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-11-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.
|
|
3
|
+
**Generated:** 2026-11-04
|
|
4
|
+
**Commit:** eb2097b0
|
|
5
|
+
**Branch:** sprint/2026-11-03-02
|
|
6
|
+
**Version:** 0.36.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|