@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,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the API key that a Delphi provider config points at, without assuming the
|
|
3
|
+
* value is in this process's environment.
|
|
4
|
+
*
|
|
5
|
+
* The credential service shipped with DSH layers environment, a provider-managed
|
|
6
|
+
* store, and `.env` files behind one `CredentialRef`. That is exactly the order the
|
|
7
|
+
* plugin needs: on the machine that prompted this work the key lived in a user-level
|
|
8
|
+
* environment variable and in `~/.dsh/.credentials.yaml`, while DSH's own process
|
|
9
|
+
* inherited neither — so `delphi-review` failed with a missing-variable error while
|
|
10
|
+
* every other gate worked.
|
|
11
|
+
*
|
|
12
|
+
* Resolution is deliberately single-layer. When `ctx.credentials` is present and
|
|
13
|
+
* reports `undefined`, this module does NOT fall back to `process.env`: the service
|
|
14
|
+
* has already consulted the environment, so a second look would resurrect a value it
|
|
15
|
+
* deliberately reported as absent and would make a DSH session resolve differently
|
|
16
|
+
* from a terminal session.
|
|
17
|
+
*/
|
|
18
|
+
/** A credential service, narrowed to the one method this module needs. */
|
|
19
|
+
export interface CredentialService {
|
|
20
|
+
resolve(ref: unknown): Promise<{
|
|
21
|
+
value: string;
|
|
22
|
+
} | undefined>;
|
|
23
|
+
}
|
|
24
|
+
/** Resolve one environment-variable-style reference to its current value. */
|
|
25
|
+
export type ApiKeyResolver = (ref: string) => Promise<string | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* Build an `ApiKeyResolver` over the available credential service.
|
|
28
|
+
*
|
|
29
|
+
* @param credentials - `ctx.credentials`, or undefined when the host does not offer
|
|
30
|
+
* one. Absence degrades to reading the process environment directly, which keeps
|
|
31
|
+
* the plugin working outside DSH (CLI use, tests).
|
|
32
|
+
* @param env - environment to read in that fallback. Injected rather than read from
|
|
33
|
+
* the global so the module stays testable offline.
|
|
34
|
+
* @param brand - converts a raw name into the service's nominal `CredentialRef`.
|
|
35
|
+
* Injected so this module needs no import of the peer package and tests can pass
|
|
36
|
+
* an identity function.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCredentialResolver(credentials: CredentialService | undefined, env: NodeJS.ProcessEnv, brand?: (value: string) => unknown): ApiKeyResolver;
|
|
39
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAAA;CAC9D;AAED,6EAA6E;AAC7E,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;AAYzE;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,iBAAiB,GAAG,SAAS,EAC1C,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,KAAK,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAA0B,GACnD,cAAc,CAkBhB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the API key that a Delphi provider config points at, without assuming the
|
|
3
|
+
* value is in this process's environment.
|
|
4
|
+
*
|
|
5
|
+
* The credential service shipped with DSH layers environment, a provider-managed
|
|
6
|
+
* store, and `.env` files behind one `CredentialRef`. That is exactly the order the
|
|
7
|
+
* plugin needs: on the machine that prompted this work the key lived in a user-level
|
|
8
|
+
* environment variable and in `~/.dsh/.credentials.yaml`, while DSH's own process
|
|
9
|
+
* inherited neither — so `delphi-review` failed with a missing-variable error while
|
|
10
|
+
* every other gate worked.
|
|
11
|
+
*
|
|
12
|
+
* Resolution is deliberately single-layer. When `ctx.credentials` is present and
|
|
13
|
+
* reports `undefined`, this module does NOT fall back to `process.env`: the service
|
|
14
|
+
* has already consulted the environment, so a second look would resurrect a value it
|
|
15
|
+
* deliberately reported as absent and would make a DSH session resolve differently
|
|
16
|
+
* from a terminal session.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Environment-variable names: a POSIX shell identifier. Anything else cannot name a
|
|
20
|
+
* credential, so it reads as "not set" rather than reaching the store.
|
|
21
|
+
*/
|
|
22
|
+
const REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
23
|
+
function isReferenceName(value) {
|
|
24
|
+
return REFERENCE_NAME.test(value);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build an `ApiKeyResolver` over the available credential service.
|
|
28
|
+
*
|
|
29
|
+
* @param credentials - `ctx.credentials`, or undefined when the host does not offer
|
|
30
|
+
* one. Absence degrades to reading the process environment directly, which keeps
|
|
31
|
+
* the plugin working outside DSH (CLI use, tests).
|
|
32
|
+
* @param env - environment to read in that fallback. Injected rather than read from
|
|
33
|
+
* the global so the module stays testable offline.
|
|
34
|
+
* @param brand - converts a raw name into the service's nominal `CredentialRef`.
|
|
35
|
+
* Injected so this module needs no import of the peer package and tests can pass
|
|
36
|
+
* an identity function.
|
|
37
|
+
*/
|
|
38
|
+
export function createCredentialResolver(credentials, env, brand = (value) => value) {
|
|
39
|
+
return async (ref) => {
|
|
40
|
+
if (!isReferenceName(ref))
|
|
41
|
+
return undefined;
|
|
42
|
+
if (credentials !== undefined) {
|
|
43
|
+
try {
|
|
44
|
+
const resolved = await credentials.resolve(brand(ref));
|
|
45
|
+
return resolved?.value;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A broken provider must not take the tool down; the caller turns the
|
|
49
|
+
// missing value into a DelphiSetupError that names the variable.
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const value = env[ref];
|
|
54
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delphi configuration loading for the DSH plugin.
|
|
3
|
+
*
|
|
4
|
+
* Reads the same `.delphi-config.json` the xp-gate skill already uses, so a
|
|
5
|
+
* project configures its reviewers once and both the skill and the DSH tool
|
|
6
|
+
* agree on who they are.
|
|
7
|
+
*
|
|
8
|
+
* @test REQ-DSH-016
|
|
9
|
+
* @intent 复用项目既有的 .delphi-config.json,把 provider/api_key/专家席位解析成
|
|
10
|
+
* DSH 工具可用的结构,并对 ${ENV} 占位符与缺失密钥给出可操作的报错。
|
|
11
|
+
* @covers AC-DSH-016-02
|
|
12
|
+
*/
|
|
13
|
+
import type { ExpertSeat, DelphiOptions } from "./delphi.js";
|
|
14
|
+
/** Shape of one expert entry in .delphi-config.json. */
|
|
15
|
+
interface RawExpert {
|
|
16
|
+
id?: string;
|
|
17
|
+
role?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
requested_model?: string;
|
|
21
|
+
max_tokens?: number;
|
|
22
|
+
maxTokens?: number;
|
|
23
|
+
/** Per-expert provider override; falls back to the profile default. */
|
|
24
|
+
provider?: string;
|
|
25
|
+
}
|
|
26
|
+
interface RawProfile {
|
|
27
|
+
provider?: string;
|
|
28
|
+
providers?: Record<string, {
|
|
29
|
+
base_url?: string;
|
|
30
|
+
baseUrl?: string;
|
|
31
|
+
api_key?: string;
|
|
32
|
+
apiKey?: string;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Experts appear in two shapes in the wild: an object keyed by role (what
|
|
36
|
+
* xp-gate actually writes) and an array. Both are accepted.
|
|
37
|
+
*/
|
|
38
|
+
experts?: RawExpert[] | Record<string, RawExpert>;
|
|
39
|
+
consensus?: {
|
|
40
|
+
threshold_percent?: number;
|
|
41
|
+
max_review_rounds?: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface RawDelphiConfig extends RawProfile {
|
|
45
|
+
active_profile?: string;
|
|
46
|
+
profiles?: Record<string, RawProfile>;
|
|
47
|
+
}
|
|
48
|
+
export interface ResolvedDelphiConfig {
|
|
49
|
+
readonly seats: ExpertSeat[];
|
|
50
|
+
readonly baseUrl: string;
|
|
51
|
+
readonly apiKey: string;
|
|
52
|
+
readonly thresholdPercent: number;
|
|
53
|
+
readonly maxRounds: number;
|
|
54
|
+
readonly provider: string;
|
|
55
|
+
}
|
|
56
|
+
/** Raised when config on disk cannot be turned into a usable review setup. */
|
|
57
|
+
export declare class DelphiSetupError extends Error {
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The result of expanding a config value: the expanded text plus what could not
|
|
61
|
+
* be resolved, kept apart by REASON.
|
|
62
|
+
*
|
|
63
|
+
* `missing` and `empty` are different problems with different remedies, so they
|
|
64
|
+
* are separate lists rather than one "did not expand" bucket (#576):
|
|
65
|
+
* missing — nobody set it: fix the environment (and, on a long-lived host,
|
|
66
|
+
* restart the process that needs to inherit it)
|
|
67
|
+
* empty — someone set it to "": fix the configuration
|
|
68
|
+
*/
|
|
69
|
+
export interface ExpandedEnv {
|
|
70
|
+
readonly value: string;
|
|
71
|
+
readonly missing: readonly string[];
|
|
72
|
+
readonly empty: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Expand `${VAR}` / `{env:VAR}` without collapsing "unset" into "empty".
|
|
76
|
+
* Supports the two spellings already present in xp-gate config files.
|
|
77
|
+
*/
|
|
78
|
+
export declare function expandEnvDetailed(value: string, env?: NodeJS.ProcessEnv): ExpandedEnv;
|
|
79
|
+
/**
|
|
80
|
+
* Expand `${VAR}` and `{env:VAR}` placeholders from the environment.
|
|
81
|
+
* Supports the two spellings already present in xp-gate config files.
|
|
82
|
+
*
|
|
83
|
+
* Kept for callers that only need the text; use {@link expandEnvDetailed} when
|
|
84
|
+
* the difference between "unset" and "set to empty" matters.
|
|
85
|
+
*/
|
|
86
|
+
export declare function expandEnv(value: string, env?: NodeJS.ProcessEnv): string;
|
|
87
|
+
/**
|
|
88
|
+
* Turn raw config into a ready-to-run review setup.
|
|
89
|
+
* Fails loudly and specifically: a missing key or an unexpanded placeholder is
|
|
90
|
+
* the most common way a Delphi run silently degrades.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveConfig(cfg: RawDelphiConfig, env?: NodeJS.ProcessEnv, apiKeyOverrides?: ApiKeyOverrides): ResolvedDelphiConfig;
|
|
93
|
+
/**
|
|
94
|
+
* Default global config location; XDG_CONFIG_HOME is an accepted override.
|
|
95
|
+
*
|
|
96
|
+
* Exported so the Gate MW validator's parity test can import THIS definition
|
|
97
|
+
* rather than restating it (2026-10-27 review, technical seat CRITICAL): the
|
|
98
|
+
* gate (`githooks/lib/validate-code-walkthrough.cjs`) needs the same path, and
|
|
99
|
+
* a second hand-written copy can drift silently. Drift here is not cosmetic —
|
|
100
|
+
* the gate would validate seats from one file while the reviewer used another,
|
|
101
|
+
* which is exactly the class of bug #574 exists to catch.
|
|
102
|
+
*/
|
|
103
|
+
export declare function defaultGlobalConfigPath(env: NodeJS.ProcessEnv): string;
|
|
104
|
+
/**
|
|
105
|
+
* Pre-resolved credential values, keyed by the environment-variable name a config
|
|
106
|
+
* referenced.
|
|
107
|
+
*
|
|
108
|
+
* The loader is synchronous and the DSH credential service is asynchronous, so a
|
|
109
|
+
* caller that resolved a key from the store hands the result in here. The override
|
|
110
|
+
* is intentionally keyed by name: a config whose `api_key` is a literal, or which
|
|
111
|
+
* names a different variable, is left untouched.
|
|
112
|
+
*/
|
|
113
|
+
type ApiKeyOverrides = Readonly<Record<string, string>>;
|
|
114
|
+
/**
|
|
115
|
+
* List the environment-variable names the config's provider api_keys reference,
|
|
116
|
+
* WITHOUT resolving any of them.
|
|
117
|
+
*
|
|
118
|
+
* `loadDelphiConfig` is synchronous while DSH's credential service is asynchronous,
|
|
119
|
+
* so the caller has to learn what to resolve before it can build the config. Reading
|
|
120
|
+
* the names is deliberately tolerant: an unset variable is exactly the case this
|
|
121
|
+
* exists for, so a name is reported whether or not it currently has a value, and a
|
|
122
|
+
* missing config yields an empty list instead of an error.
|
|
123
|
+
*/
|
|
124
|
+
export declare function listCredentialReferences(cwd: string, globalPath?: string): string[];
|
|
125
|
+
export declare function loadDelphiConfig(cwd: string, env?: NodeJS.ProcessEnv, globalPath?: string, apiKeyOverrides?: ApiKeyOverrides): ResolvedDelphiConfig;
|
|
126
|
+
/** Build the transport options a round needs from a resolved config. */
|
|
127
|
+
export declare function toDelphiOptions(cfg: ResolvedDelphiConfig): DelphiOptions;
|
|
128
|
+
export {};
|
|
129
|
+
//# sourceMappingURL=delphi-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delphi-config.d.ts","sourceRoot":"","sources":["../src/delphi-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delphi configuration loading for the DSH plugin.
|
|
3
|
+
*
|
|
4
|
+
* Reads the same `.delphi-config.json` the xp-gate skill already uses, so a
|
|
5
|
+
* project configures its reviewers once and both the skill and the DSH tool
|
|
6
|
+
* agree on who they are.
|
|
7
|
+
*
|
|
8
|
+
* @test REQ-DSH-016
|
|
9
|
+
* @intent 复用项目既有的 .delphi-config.json,把 provider/api_key/专家席位解析成
|
|
10
|
+
* DSH 工具可用的结构,并对 ${ENV} 占位符与缺失密钥给出可操作的报错。
|
|
11
|
+
* @covers AC-DSH-016-02
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
import { validateSeats } from "./delphi.js";
|
|
17
|
+
/** Raised when config on disk cannot be turned into a usable review setup. */
|
|
18
|
+
export class DelphiSetupError extends Error {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Delphi's documented defaults, applied when the config omits a consensus setting.
|
|
22
|
+
* Held as object properties so the values read as policy fields rather than stray
|
|
23
|
+
* literals.
|
|
24
|
+
*/
|
|
25
|
+
const CONSENSUS_DEFAULTS = {
|
|
26
|
+
threshold_percent: "90",
|
|
27
|
+
max_review_rounds: "5",
|
|
28
|
+
};
|
|
29
|
+
/** Placeholder spellings xp-gate configs already use. Named so the error text can
|
|
30
|
+
* quote the exact form the user wrote — a typo in the spelling is
|
|
31
|
+
* indistinguishable from an unset variable otherwise (#576). */
|
|
32
|
+
const PLACEHOLDER_PATTERNS = [
|
|
33
|
+
{ form: "shell", re: /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g },
|
|
34
|
+
{ form: "env", re: /\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g },
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Expand `${VAR}` / `{env:VAR}` without collapsing "unset" into "empty".
|
|
38
|
+
* Supports the two spellings already present in xp-gate config files.
|
|
39
|
+
*/
|
|
40
|
+
export function expandEnvDetailed(value, env = process.env) {
|
|
41
|
+
const missing = [];
|
|
42
|
+
const empty = [];
|
|
43
|
+
let out = value;
|
|
44
|
+
for (const { re } of PLACEHOLDER_PATTERNS) {
|
|
45
|
+
out = out.replace(re, (_m, name) => {
|
|
46
|
+
const found = env[name];
|
|
47
|
+
if (found === undefined) {
|
|
48
|
+
if (!missing.includes(name))
|
|
49
|
+
missing.push(name);
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
if (found === "") {
|
|
53
|
+
if (!empty.includes(name))
|
|
54
|
+
empty.push(name);
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
return found;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return { value: out, missing, empty };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Expand `${VAR}` and `{env:VAR}` placeholders from the environment.
|
|
64
|
+
* Supports the two spellings already present in xp-gate config files.
|
|
65
|
+
*
|
|
66
|
+
* Kept for callers that only need the text; use {@link expandEnvDetailed} when
|
|
67
|
+
* the difference between "unset" and "set to empty" matters.
|
|
68
|
+
*/
|
|
69
|
+
export function expandEnv(value, env = process.env) {
|
|
70
|
+
return expandEnvDetailed(value, env).value;
|
|
71
|
+
}
|
|
72
|
+
/** Pick the active profile, falling back to the top level. */
|
|
73
|
+
function selectProfile(cfg) {
|
|
74
|
+
const active = cfg.active_profile;
|
|
75
|
+
if (active && cfg.profiles?.[active])
|
|
76
|
+
return cfg.profiles[active];
|
|
77
|
+
if (cfg.profiles && Object.keys(cfg.profiles).length > 0) {
|
|
78
|
+
throw new DelphiSetupError(`No active_profile set. Available: ${Object.keys(cfg.profiles).join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
return cfg;
|
|
81
|
+
}
|
|
82
|
+
function seatFromRaw(raw, index, fallbackRole) {
|
|
83
|
+
const model = raw.model ?? raw.requested_model ?? "";
|
|
84
|
+
return {
|
|
85
|
+
id: raw.id ?? raw.name ?? `Expert ${String.fromCharCode(65 + index)}`,
|
|
86
|
+
role: raw.role ?? fallbackRole,
|
|
87
|
+
model,
|
|
88
|
+
maxTokens: raw.max_tokens ?? raw.maxTokens,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Normalize either experts shape into an ordered list with roles attached. */
|
|
92
|
+
function normalizeExperts(experts) {
|
|
93
|
+
if (!experts)
|
|
94
|
+
return [];
|
|
95
|
+
if (Array.isArray(experts)) {
|
|
96
|
+
return experts.map((raw, i) => ({ raw, role: raw.role ?? `expert${i}` }));
|
|
97
|
+
}
|
|
98
|
+
// Object form: the KEY is the role, which is how xp-gate writes it.
|
|
99
|
+
return Object.entries(experts).map(([role, raw]) => ({ raw, role }));
|
|
100
|
+
}
|
|
101
|
+
/** Resolve one provider's base_url + api_key, failing loudly. */
|
|
102
|
+
function resolveOneProvider(name, entry, env, apiKeyOverrides) {
|
|
103
|
+
const baseUrl = entry.base_url ?? entry.baseUrl ?? "";
|
|
104
|
+
if (!baseUrl)
|
|
105
|
+
throw new DelphiSetupError(`Provider "${name}" has no base_url.`);
|
|
106
|
+
const rawKey = entry.api_key ?? entry.apiKey ?? "";
|
|
107
|
+
// Find the placeholder BEFORE touching `env`: the spelling lives in the
|
|
108
|
+
// config, not the environment, so a broken `env` must not hide it.
|
|
109
|
+
const placeholder = findPlaceholder(rawKey);
|
|
110
|
+
// A caller that already resolved this variable from the authoritative store
|
|
111
|
+
// (DSH's credential service) wins over the process environment: the store is
|
|
112
|
+
// layered over env itself, so its answer is the more informed one. A literal
|
|
113
|
+
// api_key names no variable and is therefore untouched.
|
|
114
|
+
const overrideValue = placeholder ? apiKeyOverrides[placeholder.name] : undefined;
|
|
115
|
+
// Every read of `env` may fail — a caller can pass a proxy, a frozen object,
|
|
116
|
+
// or an environment that throws on access. Diagnostics must never become the
|
|
117
|
+
// reason the run dies: on any error fall back to the plainest possible
|
|
118
|
+
// message, which is still a DelphiSetupError (#576).
|
|
119
|
+
let expanded;
|
|
120
|
+
try {
|
|
121
|
+
expanded = expandEnvDetailed(rawKey, env);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
expanded = { value: "", missing: placeholder ? [placeholder.name] : [], empty: [] };
|
|
125
|
+
}
|
|
126
|
+
const apiKey = overrideValue ?? expanded.value;
|
|
127
|
+
if (!apiKey) {
|
|
128
|
+
// Name the variable rather than echoing config, which may hold a literal key.
|
|
129
|
+
throw new DelphiSetupError(placeholder
|
|
130
|
+
? describeUnusableKey(name, rawKey, placeholder, expanded, env)
|
|
131
|
+
: `Provider "${name}" has no api_key.`);
|
|
132
|
+
}
|
|
133
|
+
return { baseUrl, apiKey };
|
|
134
|
+
}
|
|
135
|
+
/** Resolve every referenced provider's base_url + api_key, failing loudly. */
|
|
136
|
+
function resolveProviders(profile, needed, env, apiKeyOverrides) {
|
|
137
|
+
if (needed.size === 0) {
|
|
138
|
+
throw new DelphiSetupError('No provider configured. Set "provider" on the profile or on each expert.');
|
|
139
|
+
}
|
|
140
|
+
const providerTable = profile.providers ?? {};
|
|
141
|
+
const resolved = new Map();
|
|
142
|
+
for (const name of needed) {
|
|
143
|
+
const entry = providerTable[name];
|
|
144
|
+
if (!entry) {
|
|
145
|
+
throw new DelphiSetupError(`Provider "${name}" is not defined under "providers". ` +
|
|
146
|
+
`Known: ${Object.keys(providerTable).join(", ") || "(none)"}`);
|
|
147
|
+
}
|
|
148
|
+
resolved.set(name, resolveOneProvider(name, entry, env, apiKeyOverrides));
|
|
149
|
+
}
|
|
150
|
+
return resolved;
|
|
151
|
+
}
|
|
152
|
+
/** Locate the first placeholder in a config value, keeping both name and spelling. */
|
|
153
|
+
function findPlaceholder(rawKey) {
|
|
154
|
+
for (const { re } of PLACEHOLDER_PATTERNS) {
|
|
155
|
+
// Fresh lastIndex each call: the shared regexes are global.
|
|
156
|
+
re.lastIndex = 0;
|
|
157
|
+
const match = re.exec(rawKey);
|
|
158
|
+
if (match)
|
|
159
|
+
return { name: match[1], spelling: match[0] };
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/** Env keys whose name matches case-insensitively. Never throws: a hostile or
|
|
164
|
+
* partially-broken `env` must not turn diagnostics into the failure (#576). */
|
|
165
|
+
function caseVariantsOf(name, env) {
|
|
166
|
+
try {
|
|
167
|
+
const lower = name.toLowerCase();
|
|
168
|
+
return Object.keys(env).filter((k) => k !== name && k.toLowerCase() === lower);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Build the message for a provider whose api_key did not expand to anything.
|
|
176
|
+
*
|
|
177
|
+
* The old text asserted "is not set in the environment" for every failure. That
|
|
178
|
+
* is a factual claim the caller cannot make from an expanded string alone: an
|
|
179
|
+
* empty result also happens when the variable IS set — to "", or set in a
|
|
180
|
+
* place this process never inherited (a long-lived host whose parent started
|
|
181
|
+
* before the variable was written; a shell that never reloaded; a systemd unit
|
|
182
|
+
* without restart; a container created without `-e`). The reader was sent to
|
|
183
|
+
* check the key, the config, and the gateway — everything except the parent
|
|
184
|
+
* process environment (#576).
|
|
185
|
+
*/
|
|
186
|
+
function describeUnusableKey(providerName, rawKey, placeholder, expanded, env) {
|
|
187
|
+
const { name, spelling } = placeholder;
|
|
188
|
+
const lines = [];
|
|
189
|
+
if (expanded.empty.includes(name)) {
|
|
190
|
+
lines.push(`API key variable ${name} is empty in the environment.`, `Provider "${providerName}" uses ${spelling} in its api_key.`, "The variable is defined but has no value — this is usually a configuration error.", `Set ${name} to a non-empty value.`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
lines.push(`API key variable ${name} is not set in the environment.`, `Provider "${providerName}" uses ${spelling} in its api_key.`);
|
|
194
|
+
const variants = caseVariantsOf(name, env);
|
|
195
|
+
if (variants.length > 0) {
|
|
196
|
+
lines.push(`Note: the environment has a variable whose name differs only in case: ` +
|
|
197
|
+
`${variants.join(", ")}. Environment variable names are case-sensitive on ` +
|
|
198
|
+
"POSIX systems, so ${name} does not match it.");
|
|
199
|
+
}
|
|
200
|
+
lines.push(`Set ${name} and restart the process that reads it. If you already set it, ` +
|
|
201
|
+
"a long-running parent may still be holding the environment it started " +
|
|
202
|
+
"with — the variable is inherited once at process start and not refreshed.");
|
|
203
|
+
}
|
|
204
|
+
if (!rawKey.includes("${") && !rawKey.includes("{env:")) {
|
|
205
|
+
// Reached only when the value expanded to "" from a literal; say so instead
|
|
206
|
+
// of naming a variable that is not referenced.
|
|
207
|
+
lines.push(`The configured api_key is a literal value and expands to an empty string.`);
|
|
208
|
+
}
|
|
209
|
+
return lines.join(" ");
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Turn raw config into a ready-to-run review setup.
|
|
213
|
+
* Fails loudly and specifically: a missing key or an unexpanded placeholder is
|
|
214
|
+
* the most common way a Delphi run silently degrades.
|
|
215
|
+
*/
|
|
216
|
+
export function resolveConfig(cfg, env = process.env, apiKeyOverrides = {}) {
|
|
217
|
+
const profile = selectProfile(cfg);
|
|
218
|
+
const normalized = normalizeExperts(profile.experts);
|
|
219
|
+
// An expert may name its own provider; otherwise there must be a profile-wide
|
|
220
|
+
// default. Resolve base_url/api_key per provider so mixed setups work.
|
|
221
|
+
const providerNames = new Set(normalized.map((e) => e.raw.provider ?? profile.provider).filter((p) => !!p));
|
|
222
|
+
const resolvedProviders = resolveProviders(profile, providerNames, env, apiKeyOverrides);
|
|
223
|
+
const seats = normalized.map((e, i) => seatFromRaw(e.raw, i, e.role));
|
|
224
|
+
// Surface an unexpanded placeholder as a config error, not a provider 404.
|
|
225
|
+
for (const s of seats) {
|
|
226
|
+
if (s.model.includes("${") || s.model.includes("{env:")) {
|
|
227
|
+
throw new DelphiSetupError(`Expert "${s.id}" model "${s.model}" is an unexpanded placeholder.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
validateSeats(seats);
|
|
231
|
+
return buildResult(profile, normalized, resolvedProviders, seats, cfg);
|
|
232
|
+
}
|
|
233
|
+
/** Assemble the final result after seats and providers validated. */
|
|
234
|
+
function buildResult(profile, normalized, resolvedProviders, seats, cfg) {
|
|
235
|
+
// All seats share one gateway in the supported setup; require agreement so a
|
|
236
|
+
// mixed list cannot silently send one seat's key to another host.
|
|
237
|
+
// Every seat resolves to the same gateway, so the first one speaks for all.
|
|
238
|
+
const [firstSeat] = normalized;
|
|
239
|
+
const chosen = resolvedProviders.get(firstSeat?.raw.provider ?? profile.provider ?? "");
|
|
240
|
+
if (!chosen)
|
|
241
|
+
throw new DelphiSetupError("Unable to resolve a provider for the first expert.");
|
|
242
|
+
for (const e of normalized) {
|
|
243
|
+
const name = e.raw.provider ?? profile.provider ?? "";
|
|
244
|
+
const other = resolvedProviders.get(name);
|
|
245
|
+
if (!other || other.baseUrl !== chosen.baseUrl) {
|
|
246
|
+
throw new DelphiSetupError(`All experts must share one gateway base_url; "${name}" differs. ` +
|
|
247
|
+
`Split the review or align the providers.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// consensus may sit on the profile or at the document root; both occur.
|
|
251
|
+
const consensus = profile.consensus ?? cfg.consensus;
|
|
252
|
+
return {
|
|
253
|
+
seats,
|
|
254
|
+
baseUrl: chosen.baseUrl,
|
|
255
|
+
apiKey: chosen.apiKey,
|
|
256
|
+
thresholdPercent: consensus?.threshold_percent ?? Number(CONSENSUS_DEFAULTS.threshold_percent),
|
|
257
|
+
maxRounds: consensus?.max_review_rounds ?? Number(CONSENSUS_DEFAULTS.max_review_rounds),
|
|
258
|
+
provider: normalized[0].raw.provider ?? profile.provider ?? "",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Default global config location; XDG_CONFIG_HOME is an accepted override.
|
|
263
|
+
*
|
|
264
|
+
* Exported so the Gate MW validator's parity test can import THIS definition
|
|
265
|
+
* rather than restating it (2026-10-27 review, technical seat CRITICAL): the
|
|
266
|
+
* gate (`githooks/lib/validate-code-walkthrough.cjs`) needs the same path, and
|
|
267
|
+
* a second hand-written copy can drift silently. Drift here is not cosmetic —
|
|
268
|
+
* the gate would validate seats from one file while the reviewer used another,
|
|
269
|
+
* which is exactly the class of bug #574 exists to catch.
|
|
270
|
+
*/
|
|
271
|
+
export function defaultGlobalConfigPath(env) {
|
|
272
|
+
// r16 MA-01 disposition: the CLI lib pins ~/.config/xp-gate, and on Linux
|
|
273
|
+
// $HOME/.config IS the XDG default — opt-in escape hatch only.
|
|
274
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "xp-gate", ".delphi-config.json");
|
|
275
|
+
}
|
|
276
|
+
/** Read and resolve `.delphi-config.json` from a project root. */
|
|
277
|
+
/** Read the raw config text from the project path, falling back to the global one. */
|
|
278
|
+
function readConfigText(cwd, globalPath) {
|
|
279
|
+
// Global fallback (AC-DSH-016-03): the machine-wide config at
|
|
280
|
+
// ~/.config/xp-gate/.delphi-config.json is the single source of truth for
|
|
281
|
+
// every project. Without it, each repo needed its own copy — and reading a
|
|
282
|
+
// stale or differently-priced config from another project's directory caused
|
|
283
|
+
// real money loss (2026-10-03: an expensive overseas model burned the daily
|
|
284
|
+
// quota). A project-local config still wins, so per-project overrides work.
|
|
285
|
+
const path = resolve(cwd, ".delphi-config.json");
|
|
286
|
+
try {
|
|
287
|
+
return readFileSync(path, "utf8");
|
|
288
|
+
}
|
|
289
|
+
catch (projectError) {
|
|
290
|
+
// Fall back ONLY when the project file is genuinely absent (ENOENT); any
|
|
291
|
+
// other read error must surface rather than silently route this run to
|
|
292
|
+
// another config. Same semantics as delphi-external-review.cjs, which
|
|
293
|
+
// gates the fallback on existsSync (walkthrough R8 MA-01).
|
|
294
|
+
if (projectError?.code !== "ENOENT") {
|
|
295
|
+
throw new DelphiSetupError(`Cannot read ${path}: ${projectError.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
return readFileSync(globalPath, "utf8");
|
|
300
|
+
}
|
|
301
|
+
catch (globalError) {
|
|
302
|
+
// r16 CI-01: mirror the project-read semantics — a global file that is
|
|
303
|
+
// absent means "no config anywhere"; any OTHER error (EACCES/EISDIR)
|
|
304
|
+
// must surface as-is rather than masquerade as a missing file.
|
|
305
|
+
if (globalError?.code !== "ENOENT") {
|
|
306
|
+
throw new DelphiSetupError(`Cannot read ${globalPath}: ${globalError.message}`);
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* List the environment-variable names the config's provider api_keys reference,
|
|
313
|
+
* WITHOUT resolving any of them.
|
|
314
|
+
*
|
|
315
|
+
* `loadDelphiConfig` is synchronous while DSH's credential service is asynchronous,
|
|
316
|
+
* so the caller has to learn what to resolve before it can build the config. Reading
|
|
317
|
+
* the names is deliberately tolerant: an unset variable is exactly the case this
|
|
318
|
+
* exists for, so a name is reported whether or not it currently has a value, and a
|
|
319
|
+
* missing config yields an empty list instead of an error.
|
|
320
|
+
*/
|
|
321
|
+
export function listCredentialReferences(cwd, globalPath = defaultGlobalConfigPath(process.env)) {
|
|
322
|
+
const raw = readConfigText(cwd, globalPath);
|
|
323
|
+
if (raw === null)
|
|
324
|
+
return [];
|
|
325
|
+
let parsed;
|
|
326
|
+
try {
|
|
327
|
+
parsed = JSON.parse(raw);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// A malformed config is reported by loadDelphiConfig with an actionable
|
|
331
|
+
// message; there is simply nothing to resolve here.
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
const names = new Set();
|
|
335
|
+
for (const profile of Object.values(parsed.profiles ?? {})) {
|
|
336
|
+
for (const provider of Object.values(profile.providers ?? {})) {
|
|
337
|
+
const placeholder = findPlaceholder(provider.api_key ?? provider.apiKey ?? "");
|
|
338
|
+
if (placeholder)
|
|
339
|
+
names.add(placeholder.name);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return [...names].sort();
|
|
343
|
+
}
|
|
344
|
+
export function loadDelphiConfig(cwd, env = process.env, globalPath = defaultGlobalConfigPath(env), apiKeyOverrides = {}) {
|
|
345
|
+
const raw = readConfigText(cwd, globalPath);
|
|
346
|
+
if (raw === null) {
|
|
347
|
+
const path = resolve(cwd, ".delphi-config.json");
|
|
348
|
+
throw new DelphiSetupError(`No .delphi-config.json found at ${path} or the global location ${globalPath}.`);
|
|
349
|
+
}
|
|
350
|
+
let parsed;
|
|
351
|
+
try {
|
|
352
|
+
parsed = JSON.parse(raw);
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
throw new DelphiSetupError(`.delphi-config.json is not valid JSON: ${e.message}`);
|
|
356
|
+
}
|
|
357
|
+
return resolveConfig(parsed, env, apiKeyOverrides);
|
|
358
|
+
}
|
|
359
|
+
/** Build the transport options a round needs from a resolved config. */
|
|
360
|
+
export function toDelphiOptions(cfg) {
|
|
361
|
+
return {
|
|
362
|
+
baseUrl: cfg.baseUrl,
|
|
363
|
+
apiKey: cfg.apiKey,
|
|
364
|
+
seats: cfg.seats,
|
|
365
|
+
thresholdPercent: cfg.thresholdPercent,
|
|
366
|
+
maxRounds: cfg.maxRounds,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
//# sourceMappingURL=delphi-config.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
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 type { DelphiOptions, RoundResult } from "./delphi.js";
|
|
11
|
+
export interface ReviewOutcome {
|
|
12
|
+
readonly rounds: readonly RoundResult[];
|
|
13
|
+
readonly consensus: boolean;
|
|
14
|
+
readonly finalRatio: number;
|
|
15
|
+
readonly totalRounds: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Run a full review. Round 1 is anonymous; each later round sees the aggregate
|
|
19
|
+
* plus whatever the caller says was addressed, then re-votes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runReview(opts: DelphiOptions, subject: string, addressed?: string, signal?: AbortSignal): Promise<ReviewOutcome>;
|
|
22
|
+
/** Human-readable transcript for the tool result. */
|
|
23
|
+
export declare function formatOutcome(outcome: ReviewOutcome): string;
|
|
24
|
+
//# sourceMappingURL=delphi-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delphi-run.d.ts","sourceRoot":"","sources":["../src/delphi-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAc,WAAW,EAAE,MAAM,aAAa,CAAA;AAoBzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IACvC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AA0CD;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,SAAK,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CA0CxB;AAED,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAiB5D"}
|