@boyingliu01/xp-gate 0.31.3 → 0.32.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/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delphi review orchestration: iterate rounds until consensus or the cap.
|
|
3
|
+
*
|
|
4
|
+
* @test REQ-DSH-016
|
|
5
|
+
* @intent 编排多轮交叉评审:首轮匿名独立评审,后续轮次反馈上一轮汇总,
|
|
6
|
+
* 直到达到共识阈值(默认 90%)或触及轮次上限(默认 5),
|
|
7
|
+
* 并把结果整理成可写成 .code-walkthrough-result.json 的证据。
|
|
8
|
+
* @covers AC-DSH-016-03
|
|
9
|
+
*/
|
|
10
|
+
import { TOOL_ABORTED } from "@deepseek-ai/dsh-tools";
|
|
11
|
+
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
12
|
+
import { runRound } from "./delphi.js";
|
|
13
|
+
/**
|
|
14
|
+
* Raise TOOL_ABORTED once the caller's signal has fired.
|
|
15
|
+
*
|
|
16
|
+
* Deliverance matches the gate tools exactly, so a cancelled review reads as a
|
|
17
|
+
* cancellation in the UI rather than as a provider outage.
|
|
18
|
+
*/
|
|
19
|
+
function assertNotAborted(signal) {
|
|
20
|
+
if (!signal?.aborted)
|
|
21
|
+
return;
|
|
22
|
+
const error = new HarnessError("tool call aborted", TOOL_ABORTED);
|
|
23
|
+
error.name = "AbortError";
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
/** Mirrors delphi.ts; declared here so orchestrators need not import internals. */
|
|
27
|
+
const DEFAULT_THRESHOLD_PERCENT = 90;
|
|
28
|
+
const DEFAULT_MAX_ROUNDS = 5;
|
|
29
|
+
/** Anonymous first round: each seat answers the same question with no peers. */
|
|
30
|
+
function roundOnePrompt(seat, subject) {
|
|
31
|
+
return (`You are the ${seat.role.toUpperCase()} expert. Review the material below INDEPENDENTLY. ` +
|
|
32
|
+
`Do not assume other reviewers exist or agree with you.\n\n${subject}\n\n` +
|
|
33
|
+
`Respond with <=300 words under the headings: "### Findings" (a markdown table of ` +
|
|
34
|
+
`ID | Severity | Description | Location | Fix, or a single NONE row), then "### VERDICT" ` +
|
|
35
|
+
`followed by exactly APPROVED or REQUEST_CHANGES, then "### CONFIDENCE" followed by an ` +
|
|
36
|
+
`integer 1-10. Approve only if you would personally ship this.`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Later rounds: feed back the aggregate so seats can revise their position.
|
|
40
|
+
* Individual reports stay anonymous -- only the tally and the addressed items
|
|
41
|
+
* are shared, which is what keeps the method honest.
|
|
42
|
+
*/
|
|
43
|
+
function laterRoundPrompt(seat, subject, previous, addressed) {
|
|
44
|
+
const tally = previous.verdicts
|
|
45
|
+
.map((v) => `- ${v.role}: ${v.verdict}`)
|
|
46
|
+
.join("\n");
|
|
47
|
+
return (`You are the ${seat.role.toUpperCase()} expert. This is ROUND ${previous.round + 1} of a ` +
|
|
48
|
+
`Delphi consensus review. Individual peer reports are anonymous; only this aggregate is shared:\n\n` +
|
|
49
|
+
`${tally}\nApprovals: ${previous.approvals}/${previous.validCount} ` +
|
|
50
|
+
`(threshold ${DEFAULT_THRESHOLD_PERCENT}%).\n\n` +
|
|
51
|
+
`${addressed ? `What changed since the last round:\n${addressed}\n\n` : ""}` +
|
|
52
|
+
`Re-examine the material and revise or hold your position.\n\n${subject}\n\n` +
|
|
53
|
+
`Respond with <=300 words: "### Round assessment" (2-3 sentences), "### Remaining blocking ` +
|
|
54
|
+
`issues" (table, or a single NONE row), "### VERDICT" followed by exactly APPROVED or ` +
|
|
55
|
+
`REQUEST_CHANGES, then "### CONFIDENCE" and an integer 1-10.`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run a full review. Round 1 is anonymous; each later round sees the aggregate
|
|
59
|
+
* plus whatever the caller says was addressed, then re-votes.
|
|
60
|
+
*/
|
|
61
|
+
export async function runReview(opts, subject, addressed = "", signal) {
|
|
62
|
+
const threshold = opts.thresholdPercent ?? DEFAULT_THRESHOLD_PERCENT;
|
|
63
|
+
const maxRounds = opts.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
64
|
+
const rounds = [];
|
|
65
|
+
// A round swallows per-seat failures into ERROR verdicts, so cancellation has to
|
|
66
|
+
// be checked around them: otherwise an abort would surface as three ERROR seats and
|
|
67
|
+
// the review would grind on through every remaining round.
|
|
68
|
+
const effective = { ...opts, signal };
|
|
69
|
+
assertNotAborted(signal);
|
|
70
|
+
for (let n = 1; n <= maxRounds; n++) {
|
|
71
|
+
const previous = rounds[rounds.length - 1];
|
|
72
|
+
const result = previous
|
|
73
|
+
? await runRound(effective, n, "You are a rigorous senior code reviewer.", (seat) => laterRoundPrompt(seat, subject, previous, addressed))
|
|
74
|
+
: await runRound(effective, 1, "You are a rigorous senior code reviewer.", (seat) => roundOnePrompt(seat, subject));
|
|
75
|
+
assertNotAborted(signal);
|
|
76
|
+
rounds.push(result);
|
|
77
|
+
// Threshold is a percentage of the whole panel, so a round with a malformed
|
|
78
|
+
// report cannot reach consensus even if every valid seat approved.
|
|
79
|
+
if (result.approvals / opts.seats.length * 100 >= threshold) {
|
|
80
|
+
return {
|
|
81
|
+
rounds,
|
|
82
|
+
consensus: true,
|
|
83
|
+
finalRatio: result.approvals / opts.seats.length,
|
|
84
|
+
totalRounds: n,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const last = rounds[rounds.length - 1];
|
|
89
|
+
return {
|
|
90
|
+
rounds,
|
|
91
|
+
consensus: false,
|
|
92
|
+
finalRatio: last ? last.approvals / opts.seats.length : 0,
|
|
93
|
+
totalRounds: rounds.length,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Human-readable transcript for the tool result. */
|
|
97
|
+
export function formatOutcome(outcome) {
|
|
98
|
+
const lines = [];
|
|
99
|
+
for (const r of outcome.rounds) {
|
|
100
|
+
lines.push(`## Round ${r.round} — approvals ${r.approvals}/${r.validCount} valid`);
|
|
101
|
+
for (const v of r.verdicts) {
|
|
102
|
+
const model = v.resolvedModel ? `${v.requestedModel} -> ${v.resolvedModel}` : v.requestedModel;
|
|
103
|
+
const note = v.error ? ` (${v.error})` : "";
|
|
104
|
+
lines.push(`- [${v.role}] ${v.id} (${model}): ${v.verdict} confidence=${v.confidence}${note}`);
|
|
105
|
+
}
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
lines.push(outcome.consensus
|
|
109
|
+
? `CONSENSUS REACHED: ${(outcome.finalRatio * 100).toFixed(0)}% after ${outcome.totalRounds} round(s).`
|
|
110
|
+
: `NO CONSENSUS: ${(outcome.finalRatio * 100).toFixed(0)}% after ${outcome.totalRounds} round(s) (cap reached).`);
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=delphi-run.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delphi cross-review over an external OpenAI-compatible provider.
|
|
3
|
+
*
|
|
4
|
+
* This is the codified form of the manual run that produced PR #456: three
|
|
5
|
+
* DISTINCT models, each asked the same question independently and anonymously,
|
|
6
|
+
* iterated until the approval ratio clears the consensus threshold.
|
|
7
|
+
*
|
|
8
|
+
* Why a plugin tool rather than a shell one-liner: the DSH tool plane is where
|
|
9
|
+
* a session actually lives, so the mechanism has to be callable from there.
|
|
10
|
+
* DSH's own `tool-subagent` cannot serve this purpose -- subagents inherit one
|
|
11
|
+
* host-configured model, so they cannot guarantee three distinct models, and
|
|
12
|
+
* Delphi's whole value rests on that distinctness.
|
|
13
|
+
*
|
|
14
|
+
* @test REQ-DSH-016
|
|
15
|
+
* @intent 用外部 provider 的多个不同模型做匿名交叉评审并判定共识,
|
|
16
|
+
* 把「多模型交叉评审」固化成 DSH 可调用的机制而非一次性脚本。
|
|
17
|
+
* @covers AC-DSH-016-01
|
|
18
|
+
*/
|
|
19
|
+
/** One expert seat. `model` is the id sent to the provider. */
|
|
20
|
+
export interface ExpertSeat {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly role: string;
|
|
23
|
+
readonly model: string;
|
|
24
|
+
/** Reasoning models burn hidden tokens, so seats need independent budgets. */
|
|
25
|
+
readonly maxTokens?: number;
|
|
26
|
+
}
|
|
27
|
+
/** A single expert's parsed verdict for one round. */
|
|
28
|
+
export interface ExpertVerdict {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly role: string;
|
|
31
|
+
readonly requestedModel: string;
|
|
32
|
+
readonly resolvedModel: string | null;
|
|
33
|
+
readonly verdict: "APPROVED" | "REQUEST_CHANGES" | "INVALID_NO_VERDICT" | "ERROR";
|
|
34
|
+
readonly confidence: number;
|
|
35
|
+
/** Only well-formed verdicts may count toward consensus. */
|
|
36
|
+
readonly counted: boolean;
|
|
37
|
+
readonly report: string;
|
|
38
|
+
readonly error?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Aggregated outcome of one round. */
|
|
41
|
+
export interface RoundResult {
|
|
42
|
+
readonly round: number;
|
|
43
|
+
readonly verdicts: readonly ExpertVerdict[];
|
|
44
|
+
readonly validCount: number;
|
|
45
|
+
readonly approvals: number;
|
|
46
|
+
readonly consensusRatio: number;
|
|
47
|
+
readonly reachedConsensus: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface DelphiOptions {
|
|
50
|
+
/** OpenAI-compatible base URL, e.g. https://host/gpt-proxy */
|
|
51
|
+
readonly baseUrl: string;
|
|
52
|
+
/** Bearer token. Callers read this from the environment; never hardcode it. */
|
|
53
|
+
readonly apiKey: string;
|
|
54
|
+
readonly seats: readonly ExpertSeat[];
|
|
55
|
+
/** Percent, 0-100. Delphi default is 90. */
|
|
56
|
+
readonly thresholdPercent?: number;
|
|
57
|
+
readonly maxRounds?: number;
|
|
58
|
+
/** Per-request ceiling; the caller owns the overall timeout. */
|
|
59
|
+
readonly timeoutMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Caller cancellation. Aborting it stops the in-flight request immediately and
|
|
62
|
+
* raises TOOL_ABORTED, matching the three gate tools. Without this a cancelled
|
|
63
|
+
* review kept spending money on requests the user had already abandoned.
|
|
64
|
+
*/
|
|
65
|
+
readonly signal?: AbortSignal;
|
|
66
|
+
readonly fetchImpl?: typeof fetch;
|
|
67
|
+
}
|
|
68
|
+
/** Raised when the configured seats cannot satisfy the Delphi invariants. */
|
|
69
|
+
export declare class DelphiConfigError extends Error {
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Reject a seat configuration that cannot produce a valid Delphi review.
|
|
73
|
+
* Checked BEFORE any network call so a misconfiguration never looks like a
|
|
74
|
+
* provider failure.
|
|
75
|
+
*/
|
|
76
|
+
export declare function validateSeats(seats: readonly ExpertSeat[]): void;
|
|
77
|
+
/**
|
|
78
|
+
* Parse an expert report into a verdict.
|
|
79
|
+
*
|
|
80
|
+
* The verdict token usually sits on the line AFTER a "### VERDICT" heading, so
|
|
81
|
+
* a same-line `VERDICT: X` pattern misses every real report. A report with no
|
|
82
|
+
* recognisable verdict is INVALID, never a guess: inferring approval from prose
|
|
83
|
+
* would let the gate pass on an assumption.
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseVerdict(text: string): {
|
|
86
|
+
verdict: ExpertVerdict["verdict"];
|
|
87
|
+
confidence: number;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Run ONE round. All seats are asked concurrently and never see each other's
|
|
91
|
+
* output, which is what makes the round anonymous.
|
|
92
|
+
*/
|
|
93
|
+
export declare function runRound(opts: DelphiOptions, round: number, system: string, buildPrompt: (seat: ExpertSeat) => string): Promise<RoundResult>;
|
|
94
|
+
//# sourceMappingURL=delphi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delphi.d.ts","sourceRoot":"","sources":["../src/delphi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,OAAO,CAAA;IACjF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,uCAAuC;AACvC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAC3C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAA;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;IACrC,4CAA4C;IAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAClC;AAeD,6EAA6E;AAC7E,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAE/C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CAoBhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG;IAC1C,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,CAAA;IACjC,UAAU,EAAE,MAAM,CAAA;CACnB,CAWA;AA0ID;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,MAAM,GACxC,OAAO,CAAC,WAAW,CAAC,CA8BtB"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delphi cross-review over an external OpenAI-compatible provider.
|
|
3
|
+
*
|
|
4
|
+
* This is the codified form of the manual run that produced PR #456: three
|
|
5
|
+
* DISTINCT models, each asked the same question independently and anonymously,
|
|
6
|
+
* iterated until the approval ratio clears the consensus threshold.
|
|
7
|
+
*
|
|
8
|
+
* Why a plugin tool rather than a shell one-liner: the DSH tool plane is where
|
|
9
|
+
* a session actually lives, so the mechanism has to be callable from there.
|
|
10
|
+
* DSH's own `tool-subagent` cannot serve this purpose -- subagents inherit one
|
|
11
|
+
* host-configured model, so they cannot guarantee three distinct models, and
|
|
12
|
+
* Delphi's whole value rests on that distinctness.
|
|
13
|
+
*
|
|
14
|
+
* @test REQ-DSH-016
|
|
15
|
+
* @intent 用外部 provider 的多个不同模型做匿名交叉评审并判定共识,
|
|
16
|
+
* 把「多模型交叉评审」固化成 DSH 可调用的机制而非一次性脚本。
|
|
17
|
+
* @covers AC-DSH-016-01
|
|
18
|
+
*/
|
|
19
|
+
/** Delphi's defining invariant: exactly three seats and a 90% approval bar. */
|
|
20
|
+
const REQUIRED_EXPERTS = 3;
|
|
21
|
+
const DEFAULT_THRESHOLD_PERCENT = 90;
|
|
22
|
+
const DEFAULT_MAX_TOKENS = 8000;
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
24
|
+
/**
|
|
25
|
+
* Sampling temperature. Low on purpose: reviewers must apply the rubric the same
|
|
26
|
+
* way every round, and creativity in a verdict is noise, not signal.
|
|
27
|
+
*/
|
|
28
|
+
const REVIEW_TEMPERATURE = 0.2;
|
|
29
|
+
/** Raised when the configured seats cannot satisfy the Delphi invariants. */
|
|
30
|
+
export class DelphiConfigError extends Error {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reject a seat configuration that cannot produce a valid Delphi review.
|
|
34
|
+
* Checked BEFORE any network call so a misconfiguration never looks like a
|
|
35
|
+
* provider failure.
|
|
36
|
+
*/
|
|
37
|
+
export function validateSeats(seats) {
|
|
38
|
+
if (seats.length !== REQUIRED_EXPERTS) {
|
|
39
|
+
throw new DelphiConfigError(`Delphi requires exactly ${REQUIRED_EXPERTS} experts, got ${seats.length}.`);
|
|
40
|
+
}
|
|
41
|
+
for (const s of seats) {
|
|
42
|
+
if (!s.model || s.model.trim() === "") {
|
|
43
|
+
throw new DelphiConfigError(`Expert "${s.id}" has no model (a local fallback cannot count).`);
|
|
44
|
+
}
|
|
45
|
+
if (s.model.includes("${")) {
|
|
46
|
+
throw new DelphiConfigError(`Expert "${s.id}" model "${s.model}" looks like an unexpanded env var.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const distinct = new Set(seats.map((s) => s.model.trim()));
|
|
50
|
+
if (distinct.size !== REQUIRED_EXPERTS) {
|
|
51
|
+
throw new DelphiConfigError(`All ${REQUIRED_EXPERTS} experts must use distinct models; got: ${[...distinct].join(", ")}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse an expert report into a verdict.
|
|
56
|
+
*
|
|
57
|
+
* The verdict token usually sits on the line AFTER a "### VERDICT" heading, so
|
|
58
|
+
* a same-line `VERDICT: X` pattern misses every real report. A report with no
|
|
59
|
+
* recognisable verdict is INVALID, never a guess: inferring approval from prose
|
|
60
|
+
* would let the gate pass on an assumption.
|
|
61
|
+
*/
|
|
62
|
+
export function parseVerdict(text) {
|
|
63
|
+
const section = /VERDICT[\s\S]{0,120}?\b(APPROVED|REQUEST_CHANGES|PASS_WITH_CAVEATS|BLOCKED)\b/i.exec(text);
|
|
64
|
+
if (!section)
|
|
65
|
+
return { verdict: "INVALID_NO_VERDICT", confidence: 0 };
|
|
66
|
+
const raw = section[1].toUpperCase();
|
|
67
|
+
// PASS_WITH_CAVEATS / BLOCKED are not approvals under the 90% rule.
|
|
68
|
+
const verdict = raw === "APPROVED" ? "APPROVED" : "REQUEST_CHANGES";
|
|
69
|
+
const c = /CONFIDENCE[\s\S]{0,40}?(\d{1,2})/i.exec(text);
|
|
70
|
+
const confidence = c ? Math.min(10, Math.max(0, Number(c[1]))) : 0;
|
|
71
|
+
return { verdict, confidence };
|
|
72
|
+
}
|
|
73
|
+
/** Cleanup for a case where no listener was attached, so there is nothing to undo. */
|
|
74
|
+
const NO_CLEANUP = () => undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Bridge caller cancellation into the controller that also owns the timeout.
|
|
77
|
+
* Bridging rather than racing two signals means one abort path, so the reason a
|
|
78
|
+
* request stopped is never ambiguous.
|
|
79
|
+
*/
|
|
80
|
+
function linkAbort(controller, callerSignal) {
|
|
81
|
+
if (!callerSignal)
|
|
82
|
+
return NO_CLEANUP;
|
|
83
|
+
const forwardAbort = () => controller.abort();
|
|
84
|
+
if (callerSignal.aborted) {
|
|
85
|
+
controller.abort();
|
|
86
|
+
return NO_CLEANUP;
|
|
87
|
+
}
|
|
88
|
+
callerSignal.addEventListener("abort", forwardAbort, { once: true });
|
|
89
|
+
return () => callerSignal.removeEventListener("abort", forwardAbort);
|
|
90
|
+
}
|
|
91
|
+
/** Extract the reviewer's text, or explain why there isn't any. */
|
|
92
|
+
function readContent(json, maxTokens) {
|
|
93
|
+
const content = json.choices?.[0]?.message?.content ?? "";
|
|
94
|
+
const finishReason = json.choices?.[0]?.finish_reason ?? "unknown";
|
|
95
|
+
// Empty content is a real failure -- typically a reasoning model that spent its
|
|
96
|
+
// whole budget on hidden tokens. Never let it pass as an empty review.
|
|
97
|
+
if (content.trim() === "") {
|
|
98
|
+
throw new Error(`empty content (finish_reason=${finishReason}, max_tokens=${maxTokens}); ` +
|
|
99
|
+
`raise max_tokens if this is a reasoning model`);
|
|
100
|
+
}
|
|
101
|
+
return { content, resolvedModel: json.model ?? null, finishReason };
|
|
102
|
+
}
|
|
103
|
+
/** Reject a non-2xx answer, keeping the body short enough to read in a verdict. */
|
|
104
|
+
async function ensureOk(res) {
|
|
105
|
+
if (res.ok)
|
|
106
|
+
return;
|
|
107
|
+
const body = await res.text().catch(() => "");
|
|
108
|
+
throw new Error(`HTTP ${res.status}: ${body.slice(0, 200)}`);
|
|
109
|
+
}
|
|
110
|
+
/** Build the chat-completions request for one seat. */
|
|
111
|
+
function buildRequest(opts, seat, system, user, maxTokens, signal) {
|
|
112
|
+
return {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
model: seat.model,
|
|
120
|
+
messages: [
|
|
121
|
+
{ role: "system", content: system },
|
|
122
|
+
{ role: "user", content: user },
|
|
123
|
+
],
|
|
124
|
+
max_tokens: maxTokens,
|
|
125
|
+
temperature: REVIEW_TEMPERATURE,
|
|
126
|
+
}),
|
|
127
|
+
signal,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** One provider call. Throws on transport failure or empty content. */
|
|
131
|
+
async function chatOnce(opts, seat, system, user) {
|
|
132
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const maxTokens = seat.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
135
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
136
|
+
const unlink = linkAbort(controller, opts.signal);
|
|
137
|
+
const url = `${opts.baseUrl.replace(/\/+$/, "")}/v1/chat/completions`;
|
|
138
|
+
try {
|
|
139
|
+
const request = buildRequest(opts, seat, system, user, maxTokens, controller.signal);
|
|
140
|
+
const res = await doFetch(url, request);
|
|
141
|
+
await ensureOk(res);
|
|
142
|
+
return readContent((await res.json()), maxTokens);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
unlink();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** A seat that failed to answer. Its silence is never counted as agreement. */
|
|
150
|
+
function failedVerdict(seat, reason) {
|
|
151
|
+
return {
|
|
152
|
+
id: seat.id,
|
|
153
|
+
role: seat.role,
|
|
154
|
+
requestedModel: seat.model,
|
|
155
|
+
resolvedModel: null,
|
|
156
|
+
verdict: "ERROR",
|
|
157
|
+
confidence: 0,
|
|
158
|
+
counted: false,
|
|
159
|
+
report: "",
|
|
160
|
+
error: reason instanceof Error ? reason.message : String(reason),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** A seat that answered: parse its verdict and keep the report as evidence. */
|
|
164
|
+
function answeredVerdict(seat, answer) {
|
|
165
|
+
const { verdict, confidence } = parseVerdict(answer.content);
|
|
166
|
+
return {
|
|
167
|
+
id: seat.id,
|
|
168
|
+
role: seat.role,
|
|
169
|
+
requestedModel: seat.model,
|
|
170
|
+
resolvedModel: answer.resolvedModel,
|
|
171
|
+
verdict,
|
|
172
|
+
confidence,
|
|
173
|
+
counted: verdict !== "INVALID_NO_VERDICT",
|
|
174
|
+
report: answer.content,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Run ONE round. All seats are asked concurrently and never see each other's
|
|
179
|
+
* output, which is what makes the round anonymous.
|
|
180
|
+
*/
|
|
181
|
+
export async function runRound(opts, round, system, buildPrompt) {
|
|
182
|
+
const settled = await Promise.allSettled(opts.seats.map(async (seat) => {
|
|
183
|
+
const r = await chatOnce(opts, seat, system, buildPrompt(seat));
|
|
184
|
+
return { seat, ...r };
|
|
185
|
+
}));
|
|
186
|
+
const verdicts = settled.map((s, i) => {
|
|
187
|
+
const seat = opts.seats[i];
|
|
188
|
+
return s.status === "fulfilled"
|
|
189
|
+
? answeredVerdict(seat, s.value)
|
|
190
|
+
: failedVerdict(seat, s.reason);
|
|
191
|
+
});
|
|
192
|
+
const counted = verdicts.filter((v) => v.counted);
|
|
193
|
+
const approvals = counted.filter((v) => v.verdict === "APPROVED").length;
|
|
194
|
+
// Consensus is measured over VALID responses: a malformed report is not a
|
|
195
|
+
// dissent, but it cannot be counted as agreement either.
|
|
196
|
+
const ratio = counted.length === 0 ? 0 : approvals / counted.length;
|
|
197
|
+
const threshold = opts.thresholdPercent ?? DEFAULT_THRESHOLD_PERCENT;
|
|
198
|
+
return {
|
|
199
|
+
round,
|
|
200
|
+
verdicts,
|
|
201
|
+
validCount: counted.length,
|
|
202
|
+
approvals,
|
|
203
|
+
consensusRatio: ratio,
|
|
204
|
+
reachedConsensus: counted.length === REQUIRED_EXPERTS && ratio * 100 >= threshold,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=delphi.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell dialect selection and quoting.
|
|
3
|
+
*
|
|
4
|
+
* DSH chooses its shell backend per platform: `dsh-base` disables
|
|
5
|
+
* `bash-sandbox`/`tool-bash` and enables `pwsh-sandbox`/`tool-pwsh` on win32.
|
|
6
|
+
* A command string written for the wrong dialect fails to parse, so callers
|
|
7
|
+
* must pick the dialect to match the executor rather than assume POSIX.
|
|
8
|
+
*/
|
|
9
|
+
export type ShellDialect = "posix" | "powershell";
|
|
10
|
+
/** Quote one argument for the target dialect. */
|
|
11
|
+
export declare function shellQuote(value: string, dialect: ShellDialect): string;
|
|
12
|
+
/**
|
|
13
|
+
* POSIX single-quote shell escaping: `'` becomes `'\''`.
|
|
14
|
+
* @deprecated Use {@link shellQuote} with an explicit dialect.
|
|
15
|
+
*/
|
|
16
|
+
export declare function shq(value: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Pick the dialect for the current platform.
|
|
19
|
+
*
|
|
20
|
+
* `process.platform` is the reliable signal available inside a tool body; it
|
|
21
|
+
* mirrors exactly where DSH enables each sandbox/executor pair.
|
|
22
|
+
*/
|
|
23
|
+
export declare function detectDialect(platform?: NodeJS.Platform): ShellDialect;
|
|
24
|
+
//# sourceMappingURL=dialect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../src/dialect.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,YAAY,CAAA;AAEjD,iDAAiD;AACjD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CASvE;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,YAAY,CAExF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell dialect selection and quoting.
|
|
3
|
+
*
|
|
4
|
+
* DSH chooses its shell backend per platform: `dsh-base` disables
|
|
5
|
+
* `bash-sandbox`/`tool-bash` and enables `pwsh-sandbox`/`tool-pwsh` on win32.
|
|
6
|
+
* A command string written for the wrong dialect fails to parse, so callers
|
|
7
|
+
* must pick the dialect to match the executor rather than assume POSIX.
|
|
8
|
+
*/
|
|
9
|
+
/** Quote one argument for the target dialect. */
|
|
10
|
+
export function shellQuote(value, dialect) {
|
|
11
|
+
if (dialect === "posix") {
|
|
12
|
+
// POSIX single-quote escaping: `'` becomes `'\''`.
|
|
13
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
14
|
+
}
|
|
15
|
+
// PowerShell single-quoted strings escape an embedded quote by doubling it.
|
|
16
|
+
// Backticks and `$(…)` are literal inside single quotes, so no further
|
|
17
|
+
// escaping is needed to keep them inert.
|
|
18
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* POSIX single-quote shell escaping: `'` becomes `'\''`.
|
|
22
|
+
* @deprecated Use {@link shellQuote} with an explicit dialect.
|
|
23
|
+
*/
|
|
24
|
+
export function shq(value) {
|
|
25
|
+
return shellQuote(value, "posix");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Pick the dialect for the current platform.
|
|
29
|
+
*
|
|
30
|
+
* `process.platform` is the reliable signal available inside a tool body; it
|
|
31
|
+
* mirrors exactly where DSH enables each sandbox/executor pair.
|
|
32
|
+
*/
|
|
33
|
+
export function detectDialect(platform = process.platform) {
|
|
34
|
+
return platform === "win32" ? "powershell" : "posix";
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=dialect.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate MW walkthrough evidence.
|
|
3
|
+
*
|
|
4
|
+
* @test REQ-DSH-012 delphi-review can produce Gate MW evidence
|
|
5
|
+
* @intent Build the .code-walkthrough-result.json document that the pre-push
|
|
6
|
+
* validator accepts, from a review the plugin actually ran.
|
|
7
|
+
* @covers AC-DSH-012-01
|
|
8
|
+
*
|
|
9
|
+
* Every field here mirrors a check in githooks/lib/validate-code-walkthrough.cjs.
|
|
10
|
+
* Two of those checks are easy to misread and are pinned by test:
|
|
11
|
+
*
|
|
12
|
+
* - result_type belongs to each EXPERT record. Putting it at the top level looks
|
|
13
|
+
* right, passes no check, and fails with a message that never mentions placement.
|
|
14
|
+
* - requested_model comes from the configured seat, never from the gateway's
|
|
15
|
+
* response. The gateway answers with its own naming (Qwen3_8-flash-next for a seat
|
|
16
|
+
* configured as local-qwen3.8-flash), so using the response tripped #574.
|
|
17
|
+
*/
|
|
18
|
+
/** The document Gate MW reads. Field names are the validator's, not ours. */
|
|
19
|
+
export interface WalkthroughEvidence {
|
|
20
|
+
readonly commit: string;
|
|
21
|
+
readonly branch: string;
|
|
22
|
+
readonly timestamp: string;
|
|
23
|
+
readonly expires: string;
|
|
24
|
+
readonly consensus_ratio: number;
|
|
25
|
+
readonly verdict: "APPROVED";
|
|
26
|
+
readonly experts: readonly EvidenceExpert[];
|
|
27
|
+
}
|
|
28
|
+
export interface EvidenceExpert {
|
|
29
|
+
readonly role: string;
|
|
30
|
+
readonly requested_model: string;
|
|
31
|
+
readonly resolved_model: string;
|
|
32
|
+
/** Per-expert, not top-level. The validator reads it here. */
|
|
33
|
+
readonly result_type: "delphi_expert_result";
|
|
34
|
+
readonly channel: "external" | "local";
|
|
35
|
+
readonly verdict: "APPROVED";
|
|
36
|
+
}
|
|
37
|
+
/** One seat as observed by the review: what we asked for, and what answered. */
|
|
38
|
+
export interface SeatOutcome {
|
|
39
|
+
readonly role: string;
|
|
40
|
+
/** The model configured for this role. Becomes requested_model. */
|
|
41
|
+
readonly model: string;
|
|
42
|
+
/** The model the gateway reported. Becomes resolved_model. */
|
|
43
|
+
readonly resolved: string;
|
|
44
|
+
}
|
|
45
|
+
export interface BuildEvidenceInput {
|
|
46
|
+
readonly head: string;
|
|
47
|
+
readonly branch: string;
|
|
48
|
+
readonly seats: readonly SeatOutcome[];
|
|
49
|
+
readonly verdict: string;
|
|
50
|
+
readonly now: Date;
|
|
51
|
+
/** Gateway URL, used to label provenance. */
|
|
52
|
+
readonly baseUrl?: string;
|
|
53
|
+
/** Observed agreement. Defaults to unanimous, which is what a 3-seat APPROVED run is. */
|
|
54
|
+
readonly consensusRatio?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Label a gateway as local or external.
|
|
58
|
+
*
|
|
59
|
+
* Only loopback counts as local: the validator uses this field as provenance, so a
|
|
60
|
+
* private-network host is still external — nothing about it is verifiable from here.
|
|
61
|
+
*/
|
|
62
|
+
export declare function evidenceChannel(baseUrl: string): "local" | "external";
|
|
63
|
+
/**
|
|
64
|
+
* Build the evidence document.
|
|
65
|
+
*
|
|
66
|
+
* Refuses on anything the validator would reject rather than emitting a document
|
|
67
|
+
* that fails at push time, where the cause is much harder to see.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildEvidence(input: BuildEvidenceInput): WalkthroughEvidence;
|
|
70
|
+
//# sourceMappingURL=evidence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAA;CAC5C;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAA;IAC5C,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAA;IACtC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAC7B;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAA;IAClB,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,yFAAyF;IACzF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAWD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAKrE;AAwBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,mBAAmB,CA8B5E"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate MW walkthrough evidence.
|
|
3
|
+
*
|
|
4
|
+
* @test REQ-DSH-012 delphi-review can produce Gate MW evidence
|
|
5
|
+
* @intent Build the .code-walkthrough-result.json document that the pre-push
|
|
6
|
+
* validator accepts, from a review the plugin actually ran.
|
|
7
|
+
* @covers AC-DSH-012-01
|
|
8
|
+
*
|
|
9
|
+
* Every field here mirrors a check in githooks/lib/validate-code-walkthrough.cjs.
|
|
10
|
+
* Two of those checks are easy to misread and are pinned by test:
|
|
11
|
+
*
|
|
12
|
+
* - result_type belongs to each EXPERT record. Putting it at the top level looks
|
|
13
|
+
* right, passes no check, and fails with a message that never mentions placement.
|
|
14
|
+
* - requested_model comes from the configured seat, never from the gateway's
|
|
15
|
+
* response. The gateway answers with its own naming (Qwen3_8-flash-next for a seat
|
|
16
|
+
* configured as local-qwen3.8-flash), so using the response tripped #574.
|
|
17
|
+
*/
|
|
18
|
+
/** The validator requires exactly three seats. */
|
|
19
|
+
const REQUIRED_SEATS = 3;
|
|
20
|
+
/** Evidence is valid for exactly this long; the validator checks the delta. */
|
|
21
|
+
const VALIDITY_MS = 3_600_000;
|
|
22
|
+
/** The validator rejects anything below this. */
|
|
23
|
+
const MIN_CONSENSUS = 0.9;
|
|
24
|
+
/**
|
|
25
|
+
* Label a gateway as local or external.
|
|
26
|
+
*
|
|
27
|
+
* Only loopback counts as local: the validator uses this field as provenance, so a
|
|
28
|
+
* private-network host is still external — nothing about it is verifiable from here.
|
|
29
|
+
*/
|
|
30
|
+
export function evidenceChannel(baseUrl) {
|
|
31
|
+
const lowered = baseUrl.toLowerCase();
|
|
32
|
+
return lowered.includes("localhost") || lowered.includes("127.0.0.1") || lowered.includes("[::1]")
|
|
33
|
+
? "local"
|
|
34
|
+
: "external";
|
|
35
|
+
}
|
|
36
|
+
/** Refuse to emit a document the validator would reject, and say which field. */
|
|
37
|
+
function assertSeatsUsable(seats) {
|
|
38
|
+
if (seats.length !== REQUIRED_SEATS) {
|
|
39
|
+
throw new Error(`evidence needs exactly ${REQUIRED_SEATS} seats; got ${seats.length}`);
|
|
40
|
+
}
|
|
41
|
+
const requested = new Set();
|
|
42
|
+
const resolved = new Set();
|
|
43
|
+
for (const seat of seats) {
|
|
44
|
+
if (seat.resolved.trim() === "") {
|
|
45
|
+
throw new Error(`seat "${seat.role}" has no resolved_model; a failed seat cannot produce evidence`);
|
|
46
|
+
}
|
|
47
|
+
// Both are checked case-insensitively by some validator paths, so assert on the
|
|
48
|
+
// stricter reading: duplicated IDs mean one model answered twice.
|
|
49
|
+
if (requested.has(seat.model.trim()))
|
|
50
|
+
throw new Error(`duplicate requested_model "${seat.model}"`);
|
|
51
|
+
if (resolved.has(seat.resolved.trim().toLowerCase())) {
|
|
52
|
+
throw new Error(`duplicate resolved_model "${seat.resolved}"`);
|
|
53
|
+
}
|
|
54
|
+
requested.add(seat.model.trim());
|
|
55
|
+
resolved.add(seat.resolved.trim().toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the evidence document.
|
|
60
|
+
*
|
|
61
|
+
* Refuses on anything the validator would reject rather than emitting a document
|
|
62
|
+
* that fails at push time, where the cause is much harder to see.
|
|
63
|
+
*/
|
|
64
|
+
export function buildEvidence(input) {
|
|
65
|
+
if (input.verdict !== "APPROVED") {
|
|
66
|
+
throw new Error(`evidence requires an APPROVED review; got ${input.verdict}`);
|
|
67
|
+
}
|
|
68
|
+
assertSeatsUsable(input.seats);
|
|
69
|
+
const ratio = input.consensusRatio ?? 1;
|
|
70
|
+
if (ratio < MIN_CONSENSUS || ratio > 1) {
|
|
71
|
+
throw new Error(`consensus_ratio must be ${MIN_CONSENSUS} through 1; got ${ratio}`);
|
|
72
|
+
}
|
|
73
|
+
const channel = evidenceChannel(input.baseUrl ?? "");
|
|
74
|
+
const expires = new Date(input.now.getTime() + VALIDITY_MS);
|
|
75
|
+
return {
|
|
76
|
+
commit: input.head,
|
|
77
|
+
branch: input.branch,
|
|
78
|
+
timestamp: input.now.toISOString(),
|
|
79
|
+
expires: expires.toISOString(),
|
|
80
|
+
consensus_ratio: ratio,
|
|
81
|
+
verdict: "APPROVED",
|
|
82
|
+
experts: input.seats.map((seat) => ({
|
|
83
|
+
role: seat.role,
|
|
84
|
+
requested_model: seat.model,
|
|
85
|
+
resolved_model: seat.resolved,
|
|
86
|
+
result_type: "delphi_expert_result",
|
|
87
|
+
channel,
|
|
88
|
+
verdict: "APPROVED",
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=evidence.js.map
|