dsh-model-jury 0.1.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/CHANGELOG.md +22 -0
- package/COMPATIBILITY.md +17 -0
- package/CONTRIBUTING.md +31 -0
- package/DEMO.md +64 -0
- package/LICENSE +21 -0
- package/PRE_FLIGHT.md +39 -0
- package/README.md +196 -0
- package/REAL_DEMO.md +78 -0
- package/SECURITY.md +9 -0
- package/cordis.patch.yml +30 -0
- package/lib/command.d.ts +13 -0
- package/lib/command.js +45 -0
- package/lib/config.d.ts +35 -0
- package/lib/config.js +39 -0
- package/lib/council-service.d.ts +33 -0
- package/lib/council-service.js +86 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +12 -0
- package/lib/persistence/run-store.d.ts +15 -0
- package/lib/persistence/run-store.js +55 -0
- package/lib/protocol/aggregate.d.ts +40 -0
- package/lib/protocol/aggregate.js +87 -0
- package/lib/protocol/anonymize.d.ts +20 -0
- package/lib/protocol/anonymize.js +60 -0
- package/lib/protocol/prompts.d.ts +12 -0
- package/lib/protocol/prompts.js +102 -0
- package/lib/protocol/report.d.ts +4 -0
- package/lib/protocol/report.js +101 -0
- package/lib/protocol/schemas.d.ts +66 -0
- package/lib/protocol/schemas.js +48 -0
- package/lib/protocol/state-machine.d.ts +41 -0
- package/lib/protocol/state-machine.js +109 -0
- package/lib/seats/codex-seat.d.ts +17 -0
- package/lib/seats/codex-seat.js +81 -0
- package/lib/seats/llm-seat.d.ts +17 -0
- package/lib/seats/llm-seat.js +96 -0
- package/lib/seats/structured.d.ts +22 -0
- package/lib/seats/structured.js +126 -0
- package/lib/seats/types.d.ts +58 -0
- package/lib/seats/types.js +16 -0
- package/lib/seats/unavailable-seat.d.ts +13 -0
- package/lib/seats/unavailable-seat.js +40 -0
- package/package.json +80 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
3
|
+
import type { Config } from './config.js';
|
|
4
|
+
import { type CouncilRunResult } from './protocol/state-machine.js';
|
|
5
|
+
import type { CouncilStyle } from './protocol/prompts.js';
|
|
6
|
+
import type { CouncilSeatId, SeatHealth } from './seats/types.js';
|
|
7
|
+
/** Persisted successful Council run returned to the command adapter. */
|
|
8
|
+
export interface StoredCouncilRun {
|
|
9
|
+
readonly result: CouncilRunResult;
|
|
10
|
+
readonly report: string;
|
|
11
|
+
readonly runId: string;
|
|
12
|
+
readonly directory: string;
|
|
13
|
+
}
|
|
14
|
+
/** Complete doctor result for all configured seats. */
|
|
15
|
+
export interface CouncilDoctorResult {
|
|
16
|
+
readonly ready: boolean;
|
|
17
|
+
readonly seats: Readonly<Record<CouncilSeatId, SeatHealth>>;
|
|
18
|
+
readonly codexReadOnlyEnforcement: 'prompt-only';
|
|
19
|
+
}
|
|
20
|
+
/** Orchestrates seats, deterministic protocol, persistence, doctor, and reporting. */
|
|
21
|
+
export declare class CouncilService {
|
|
22
|
+
private readonly ctx;
|
|
23
|
+
private readonly config;
|
|
24
|
+
private readonly seats;
|
|
25
|
+
constructor(ctx: Context, config: Config);
|
|
26
|
+
/** Run real minimal provider probes concurrently. */
|
|
27
|
+
doctor(parent: Agent, signal: AbortSignal): Promise<CouncilDoctorResult>;
|
|
28
|
+
/** Execute and persist one complete Council run. */
|
|
29
|
+
run(question: string, style: CouncilStyle, parent: Agent, signal: AbortSignal): Promise<StoredCouncilRun>;
|
|
30
|
+
}
|
|
31
|
+
/** Render doctor status without ever printing credential values. */
|
|
32
|
+
export declare function renderDoctor(result: CouncilDoctorResult): string;
|
|
33
|
+
//# sourceMappingURL=council-service.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { RunStore } from './persistence/run-store.js';
|
|
2
|
+
import { renderReport } from './protocol/report.js';
|
|
3
|
+
import { CouncilStateMachine } from './protocol/state-machine.js';
|
|
4
|
+
import { CodexSeat } from './seats/codex-seat.js';
|
|
5
|
+
import { LlmSeat } from './seats/llm-seat.js';
|
|
6
|
+
import { SEAT_NAMES } from './seats/types.js';
|
|
7
|
+
import { UnavailableSeat } from './seats/unavailable-seat.js';
|
|
8
|
+
function publicConfig(config) {
|
|
9
|
+
return {
|
|
10
|
+
storageDir: config.storageDir,
|
|
11
|
+
maxQuestionChars: config.maxQuestionChars,
|
|
12
|
+
maxFieldChars: config.maxFieldChars,
|
|
13
|
+
codex: config.codex,
|
|
14
|
+
glm: config.glm,
|
|
15
|
+
deepseek: config.deepseek,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Orchestrates seats, deterministic protocol, persistence, doctor, and reporting. */
|
|
19
|
+
export class CouncilService {
|
|
20
|
+
ctx;
|
|
21
|
+
config;
|
|
22
|
+
seats;
|
|
23
|
+
constructor(ctx, config) {
|
|
24
|
+
this.ctx = ctx;
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.seats = [
|
|
27
|
+
new CodexSeat(ctx, config.codex),
|
|
28
|
+
config.glm.model === null
|
|
29
|
+
? new UnavailableSeat('glm', config.glm.provider, 'GLM model is not configured in Council settings')
|
|
30
|
+
: new LlmSeat('glm', ctx, config.glm),
|
|
31
|
+
config.deepseek.model === null
|
|
32
|
+
? new UnavailableSeat('deepseek', config.deepseek.provider, 'DeepSeek model is not configured in Council settings')
|
|
33
|
+
: new LlmSeat('deepseek', ctx, config.deepseek),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
/** Run real minimal provider probes concurrently. */
|
|
37
|
+
async doctor(parent, signal) {
|
|
38
|
+
const entries = await Promise.all(this.seats.map(async (seat) => [seat.id, await seat.healthCheck(parent, signal)]));
|
|
39
|
+
const seats = Object.fromEntries(entries);
|
|
40
|
+
return {
|
|
41
|
+
ready: Object.values(seats).every(health => health.ok),
|
|
42
|
+
seats,
|
|
43
|
+
codexReadOnlyEnforcement: 'prompt-only',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Execute and persist one complete Council run. */
|
|
47
|
+
async run(question, style, parent, signal) {
|
|
48
|
+
const normalized = question.trim();
|
|
49
|
+
if (normalized.length === 0)
|
|
50
|
+
throw new Error('Council question must not be empty');
|
|
51
|
+
if (normalized.length > this.config.maxQuestionChars) {
|
|
52
|
+
throw new Error(`Council question exceeds ${this.config.maxQuestionChars} characters`);
|
|
53
|
+
}
|
|
54
|
+
const workspace = parent.session.header.cwd ?? process.cwd();
|
|
55
|
+
const store = await RunStore.create(workspace, this.config.storageDir, normalized, style, publicConfig(this.config));
|
|
56
|
+
const observer = {
|
|
57
|
+
stateChanged: state => store.writeJson('state.json', { state, updatedAt: new Date().toISOString() }),
|
|
58
|
+
prompt: (round, seat, prompt) => store.writeText(`prompts/${round}/${seat}.txt`, prompt),
|
|
59
|
+
response: (round, seat, response) => store.writeJson(`${round}/${seat}.json`, response),
|
|
60
|
+
failure: (failure, metadata) => store.writeJson(`${failure.round}/${failure.seat}.json`, { success: false, failure, ...metadata === undefined ? {} : { metadata } }),
|
|
61
|
+
redistribution: metadata => store.writeJson('redistribution.json', metadata),
|
|
62
|
+
};
|
|
63
|
+
const machine = new CouncilStateMachine(this.seats, this.config.maxFieldChars, observer);
|
|
64
|
+
const result = await machine.run(normalized, style, parent, signal);
|
|
65
|
+
const report = renderReport(result, store.id);
|
|
66
|
+
const calls = [...result.round1.values(), ...result.round2.values(), ...result.round3.values()]
|
|
67
|
+
.map(response => response.metadata);
|
|
68
|
+
await Promise.all([
|
|
69
|
+
store.writeJson('verdict.json', result.verdict),
|
|
70
|
+
store.writeJson('calls.json', calls),
|
|
71
|
+
store.writeText('report.md', report),
|
|
72
|
+
]);
|
|
73
|
+
return { result, report, runId: store.id, directory: store.directory };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Render doctor status without ever printing credential values. */
|
|
77
|
+
export function renderDoctor(result) {
|
|
78
|
+
const lines = ['Model Jury Providers', ''];
|
|
79
|
+
for (const seat of ['gpt', 'glm', 'deepseek']) {
|
|
80
|
+
const health = result.seats[seat];
|
|
81
|
+
lines.push(`${seat === 'gpt' ? 'GPT / Codex' : SEAT_NAMES[seat]}:`, health.ok ? 'PASS' : 'FAIL', `Provider: ${health.provider}`, `Model: ${health.model ?? 'not configured'}`, ...(seat === 'gpt' && health.ok ? ['Authentication: native Codex / ChatGPT subscription'] : []), `Duration: ${health.durationMs} ms`, `Diagnostic: ${health.detail}`, '');
|
|
82
|
+
}
|
|
83
|
+
lines.push('Codex read-only enforcement:', 'LIMITATION — DSH exposes no strict read-only Codex subagent mode; Model Jury uses mutation-prohibiting prompts.', '', `Model Jury: ${result.ready ? 'READY' : 'NOT READY'}`);
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=council-service.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { Config, type Config as CouncilConfig } from './config.js';
|
|
3
|
+
export declare const name = "dsh-model-jury";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export { Config };
|
|
6
|
+
export type { CouncilConfig };
|
|
7
|
+
export { CouncilService } from './council-service.js';
|
|
8
|
+
export type { CouncilRunResult } from './protocol/state-machine.js';
|
|
9
|
+
export type { CouncilVerdict } from './protocol/aggregate.js';
|
|
10
|
+
export type { CouncilSeat, CouncilSeatRequest, CouncilSeatResponse } from './seats/types.js';
|
|
11
|
+
/** Mount the Council protocol service and the public Model Jury command. */
|
|
12
|
+
export declare function apply(ctx: Context, config: CouncilConfig): void;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { registerCouncilCommand } from './command.js';
|
|
2
|
+
import { Config } from './config.js';
|
|
3
|
+
import { CouncilService } from './council-service.js';
|
|
4
|
+
export const name = 'dsh-model-jury';
|
|
5
|
+
export const inject = ['commands', 'llm', 'subagents'];
|
|
6
|
+
export { Config };
|
|
7
|
+
export { CouncilService } from './council-service.js';
|
|
8
|
+
/** Mount the Council protocol service and the public Model Jury command. */
|
|
9
|
+
export function apply(ctx, config) {
|
|
10
|
+
registerCouncilCommand(ctx, new CouncilService(ctx, config));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Replace credential-shaped fields before any value reaches disk. */
|
|
2
|
+
export declare function redactSecrets(value: unknown): unknown;
|
|
3
|
+
/** Durable owner-only artifact store for one Council run. */
|
|
4
|
+
export declare class RunStore {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly directory: string;
|
|
7
|
+
private constructor();
|
|
8
|
+
/** Create the run tree and persist safe request/config snapshots. */
|
|
9
|
+
static create(workspace: string, storageDir: string, question: string, style: string, config: unknown, now?: Date): Promise<RunStore>;
|
|
10
|
+
/** Atomically persist a redacted JSON artifact beneath this run. */
|
|
11
|
+
writeJson(relativePath: string, value: unknown): Promise<void>;
|
|
12
|
+
/** Atomically persist a text artifact beneath this run. */
|
|
13
|
+
writeText(relativePath: string, text: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=run-store.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
const SECRET_FIELD = /^(?:api[_-]?key|token|access[_-]?token|refresh[_-]?token|authorization|credentials?|cookie|password|client[_-]?secret|secret)$/iu;
|
|
5
|
+
/** Replace credential-shaped fields before any value reaches disk. */
|
|
6
|
+
export function redactSecrets(value) {
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return value.map(redactSecrets);
|
|
9
|
+
if (typeof value !== 'object' || value === null)
|
|
10
|
+
return value;
|
|
11
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
12
|
+
key,
|
|
13
|
+
SECRET_FIELD.test(key) ? '[REDACTED]' : redactSecrets(entry),
|
|
14
|
+
]));
|
|
15
|
+
}
|
|
16
|
+
function runId(now) {
|
|
17
|
+
const timestamp = now.toISOString().replace(/[-:.]/gu, '').replace('Z', 'Z');
|
|
18
|
+
return `${timestamp}-${randomUUID().slice(0, 8)}`;
|
|
19
|
+
}
|
|
20
|
+
/** Durable owner-only artifact store for one Council run. */
|
|
21
|
+
export class RunStore {
|
|
22
|
+
id;
|
|
23
|
+
directory;
|
|
24
|
+
constructor(root, id) {
|
|
25
|
+
this.id = id;
|
|
26
|
+
this.directory = join(root, 'runs', id);
|
|
27
|
+
}
|
|
28
|
+
/** Create the run tree and persist safe request/config snapshots. */
|
|
29
|
+
static async create(workspace, storageDir, question, style, config, now = new Date()) {
|
|
30
|
+
const store = new RunStore(resolve(workspace, storageDir), runId(now));
|
|
31
|
+
await mkdir(store.directory, { recursive: true, mode: 0o700 });
|
|
32
|
+
await Promise.all([
|
|
33
|
+
store.writeJson('request.json', { question, style, createdAt: now.toISOString() }),
|
|
34
|
+
store.writeJson('config.json', config),
|
|
35
|
+
]);
|
|
36
|
+
return store;
|
|
37
|
+
}
|
|
38
|
+
/** Atomically persist a redacted JSON artifact beneath this run. */
|
|
39
|
+
async writeJson(relativePath, value) {
|
|
40
|
+
const path = join(this.directory, relativePath);
|
|
41
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
42
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
43
|
+
await writeFile(temporary, `${JSON.stringify(redactSecrets(value), null, 2)}\n`, { mode: 0o600 });
|
|
44
|
+
await rename(temporary, path);
|
|
45
|
+
}
|
|
46
|
+
/** Atomically persist a text artifact beneath this run. */
|
|
47
|
+
async writeText(relativePath, text) {
|
|
48
|
+
const path = join(this.directory, relativePath);
|
|
49
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
50
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
51
|
+
await writeFile(temporary, text.endsWith('\n') ? text : `${text}\n`, { mode: 0o600 });
|
|
52
|
+
await rename(temporary, path);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=run-store.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CouncilSeatId } from '../seats/types.js';
|
|
2
|
+
import type { AnonymizationMap } from './anonymize.js';
|
|
3
|
+
import type { PreferredPosition, Round1Response, Round3Response } from './schemas.js';
|
|
4
|
+
/** One failed provider call retained for the verdict. */
|
|
5
|
+
export interface CouncilFailure {
|
|
6
|
+
readonly seat: CouncilSeatId;
|
|
7
|
+
readonly round: 'round1' | 'round2' | 'round3';
|
|
8
|
+
readonly error: string;
|
|
9
|
+
}
|
|
10
|
+
/** Deterministic aggregate over structured final votes. */
|
|
11
|
+
export interface CouncilVerdict {
|
|
12
|
+
readonly valid: boolean;
|
|
13
|
+
readonly result: 'unanimous' | 'majority' | 'split' | 'degraded' | 'failed';
|
|
14
|
+
readonly voteStatus: 'unanimous' | 'majority' | 'split';
|
|
15
|
+
readonly quorum: '3/3' | '2/3' | '0-1/3';
|
|
16
|
+
readonly successfulSeats: readonly CouncilSeatId[];
|
|
17
|
+
readonly failedSeats: readonly CouncilSeatId[];
|
|
18
|
+
readonly votes: Readonly<Record<string, number>>;
|
|
19
|
+
readonly winningChoice: PreferredPosition | null;
|
|
20
|
+
readonly recommendation: string | null;
|
|
21
|
+
readonly consensus: readonly string[];
|
|
22
|
+
readonly majorityView: readonly string[];
|
|
23
|
+
readonly minorityView: readonly string[];
|
|
24
|
+
readonly criticalRisks: readonly {
|
|
25
|
+
seat: CouncilSeatId;
|
|
26
|
+
category: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
}[];
|
|
29
|
+
readonly criticalReviewRequired: boolean;
|
|
30
|
+
readonly unresolvedQuestions: readonly string[];
|
|
31
|
+
readonly evidenceStillNeeded: readonly string[];
|
|
32
|
+
readonly confidence: Partial<Record<CouncilSeatId, number>>;
|
|
33
|
+
readonly changedMindCount: number;
|
|
34
|
+
readonly failures: readonly CouncilFailure[];
|
|
35
|
+
readonly anonymization: AnonymizationMap;
|
|
36
|
+
readonly hybridSemanticCaveat: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Aggregate explicit structured choices without semantic clustering. */
|
|
39
|
+
export declare function aggregateCouncil(finals: ReadonlyMap<CouncilSeatId, Round3Response>, round1: ReadonlyMap<CouncilSeatId, Round1Response>, mapping: AnonymizationMap, failures: readonly CouncilFailure[]): CouncilVerdict;
|
|
40
|
+
//# sourceMappingURL=aggregate.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { SEAT_NAMES } from '../seats/types.js';
|
|
2
|
+
function unique(values) {
|
|
3
|
+
return [...new Set(values.map(value => value.trim()).filter(Boolean))];
|
|
4
|
+
}
|
|
5
|
+
function consensusPoints(finals) {
|
|
6
|
+
const counts = new Map();
|
|
7
|
+
for (const response of finals.values()) {
|
|
8
|
+
for (const point of unique(response.agreement_points)) {
|
|
9
|
+
const key = point.toLocaleLowerCase();
|
|
10
|
+
const hit = counts.get(key);
|
|
11
|
+
counts.set(key, { text: hit?.text ?? point, count: (hit?.count ?? 0) + 1 });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return [...counts.values()].filter(hit => hit.count >= 2).map(hit => hit.text);
|
|
15
|
+
}
|
|
16
|
+
function normalizedRiskCategory(category) {
|
|
17
|
+
return category.trim().toLocaleLowerCase().replace(/[\s_-]+/gu, ' ');
|
|
18
|
+
}
|
|
19
|
+
/** Aggregate explicit structured choices without semantic clustering. */
|
|
20
|
+
export function aggregateCouncil(finals, round1, mapping, failures) {
|
|
21
|
+
const successfulSeats = [...finals.keys()];
|
|
22
|
+
const failedSeats = ['gpt', 'glm', 'deepseek'].filter(seat => !finals.has(seat));
|
|
23
|
+
const votes = new Map();
|
|
24
|
+
for (const response of finals.values()) {
|
|
25
|
+
votes.set(response.preferred_position, (votes.get(response.preferred_position) ?? 0) + 1);
|
|
26
|
+
}
|
|
27
|
+
const orderedVotes = [...votes.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
28
|
+
const top = orderedVotes[0];
|
|
29
|
+
const successCount = successfulSeats.length;
|
|
30
|
+
const topCount = top?.[1] ?? 0;
|
|
31
|
+
const voteStatus = successCount > 0 && topCount === successCount
|
|
32
|
+
? 'unanimous'
|
|
33
|
+
: topCount >= 2
|
|
34
|
+
? 'majority'
|
|
35
|
+
: 'split';
|
|
36
|
+
const quorum = successCount === 3 ? '3/3' : successCount === 2 ? '2/3' : '0-1/3';
|
|
37
|
+
const valid = successCount >= 2;
|
|
38
|
+
const result = !valid ? 'failed' : successCount === 2 ? 'degraded' : voteStatus;
|
|
39
|
+
const winningChoice = voteStatus === 'split' ? null : top?.[0] ?? null;
|
|
40
|
+
const winners = winningChoice === null
|
|
41
|
+
? []
|
|
42
|
+
: successfulSeats.filter(seat => finals.get(seat)?.preferred_position === winningChoice);
|
|
43
|
+
const dissenters = successfulSeats.filter(seat => !winners.includes(seat));
|
|
44
|
+
const sortedWinners = [...winners].sort((left, right) => ((finals.get(right)?.confidence ?? 0) - (finals.get(left)?.confidence ?? 0)));
|
|
45
|
+
const recommendation = sortedWinners[0] === undefined
|
|
46
|
+
? null
|
|
47
|
+
: finals.get(sortedWinners[0])?.final_recommendation ?? null;
|
|
48
|
+
const majorityView = winners.map(seat => `${SEAT_NAMES[seat]}: ${finals.get(seat)?.final_recommendation ?? ''}`);
|
|
49
|
+
const minorityView = dissenters.map(seat => `${SEAT_NAMES[seat]}: ${finals.get(seat)?.final_recommendation ?? ''}`);
|
|
50
|
+
const criticalRisks = successfulSeats.flatMap((seat) => {
|
|
51
|
+
const risk = finals.get(seat)?.critical_risk;
|
|
52
|
+
return risk?.active && risk.category !== null && risk.reason !== null
|
|
53
|
+
? [{ seat, category: risk.category, reason: risk.reason }]
|
|
54
|
+
: [];
|
|
55
|
+
});
|
|
56
|
+
const riskCounts = new Map();
|
|
57
|
+
for (const risk of criticalRisks) {
|
|
58
|
+
const key = normalizedRiskCategory(risk.category);
|
|
59
|
+
riskCounts.set(key, (riskCounts.get(key) ?? 0) + 1);
|
|
60
|
+
}
|
|
61
|
+
const criticalReviewRequired = [...riskCounts.values()].some(count => count >= 2);
|
|
62
|
+
const confidence = Object.fromEntries(successfulSeats.map(seat => [seat, finals.get(seat)?.confidence ?? 0]));
|
|
63
|
+
return {
|
|
64
|
+
valid,
|
|
65
|
+
result,
|
|
66
|
+
voteStatus,
|
|
67
|
+
quorum,
|
|
68
|
+
successfulSeats,
|
|
69
|
+
failedSeats,
|
|
70
|
+
votes: Object.fromEntries(orderedVotes),
|
|
71
|
+
winningChoice,
|
|
72
|
+
recommendation,
|
|
73
|
+
consensus: consensusPoints(finals),
|
|
74
|
+
majorityView,
|
|
75
|
+
minorityView,
|
|
76
|
+
criticalRisks,
|
|
77
|
+
criticalReviewRequired,
|
|
78
|
+
unresolvedQuestions: unique([...finals.values()].flatMap(response => response.disagreement_points)),
|
|
79
|
+
evidenceStillNeeded: unique([...round1.values()].flatMap(response => response.evidence_needed)),
|
|
80
|
+
confidence,
|
|
81
|
+
changedMindCount: [...finals.values()].filter(response => response.changed_mind).length,
|
|
82
|
+
failures,
|
|
83
|
+
anonymization: mapping,
|
|
84
|
+
hybridSemanticCaveat: winningChoice === 'hybrid' && topCount >= 2,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=aggregate.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CouncilSeatId } from '../seats/types.js';
|
|
2
|
+
/** Anonymous position labels used in review and votes. */
|
|
3
|
+
export type PositionLabel = 'P1' | 'P2' | 'P3';
|
|
4
|
+
/** Per-run private seat-to-position assignment. */
|
|
5
|
+
export type AnonymizationMap = Readonly<Record<CouncilSeatId, PositionLabel>>;
|
|
6
|
+
/** Randomize a complete three-seat mapping with an injectable test source. */
|
|
7
|
+
export declare function createAnonymizationMap(randomIndex?: (upperExclusive: number) => number): AnonymizationMap;
|
|
8
|
+
/** Metadata describing bounded or identity-scrubbed redistribution fields. */
|
|
9
|
+
export interface RedistributionMetadata {
|
|
10
|
+
readonly truncations: readonly string[];
|
|
11
|
+
readonly identityRedactions: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/** Sanitized packet material plus explicit transformation metadata. */
|
|
14
|
+
export interface SanitizedPacket<T> {
|
|
15
|
+
readonly value: T;
|
|
16
|
+
readonly metadata: RedistributionMetadata;
|
|
17
|
+
}
|
|
18
|
+
/** Bound strings and remove provider/model names before cross-seat redistribution. */
|
|
19
|
+
export declare function sanitizeForRedistribution<T>(input: T, maxFieldChars: number, identities: readonly string[]): SanitizedPacket<T>;
|
|
20
|
+
//# sourceMappingURL=anonymize.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
2
|
+
/** Randomize a complete three-seat mapping with an injectable test source. */
|
|
3
|
+
export function createAnonymizationMap(randomIndex = randomInt) {
|
|
4
|
+
const labels = ['P1', 'P2', 'P3'];
|
|
5
|
+
for (let index = labels.length - 1; index > 0; index -= 1) {
|
|
6
|
+
const swap = randomIndex(index + 1);
|
|
7
|
+
const current = labels[index];
|
|
8
|
+
const selected = labels[swap];
|
|
9
|
+
if (current === undefined || selected === undefined)
|
|
10
|
+
throw new Error('invalid anonymization random index');
|
|
11
|
+
labels[index] = selected;
|
|
12
|
+
labels[swap] = current;
|
|
13
|
+
}
|
|
14
|
+
const [gpt, glm, deepseek] = labels;
|
|
15
|
+
if (gpt === undefined || glm === undefined || deepseek === undefined)
|
|
16
|
+
throw new Error('incomplete anonymization mapping');
|
|
17
|
+
return Object.freeze({ gpt, glm, deepseek });
|
|
18
|
+
}
|
|
19
|
+
function identityPattern(identities) {
|
|
20
|
+
const escaped = identities
|
|
21
|
+
.map(identity => identity.trim())
|
|
22
|
+
.filter(identity => identity.length >= 3)
|
|
23
|
+
.sort((left, right) => right.length - left.length)
|
|
24
|
+
.map(identity => identity.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'));
|
|
25
|
+
return escaped.length === 0 ? undefined : new RegExp(`\\b(?:${escaped.join('|')})\\b`, 'giu');
|
|
26
|
+
}
|
|
27
|
+
/** Bound strings and remove provider/model names before cross-seat redistribution. */
|
|
28
|
+
export function sanitizeForRedistribution(input, maxFieldChars, identities) {
|
|
29
|
+
const truncations = [];
|
|
30
|
+
const identityRedactions = [];
|
|
31
|
+
const pattern = identityPattern(identities);
|
|
32
|
+
const visit = (value, path) => {
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
const scrubbed = pattern === undefined
|
|
35
|
+
? value
|
|
36
|
+
: value.replace(pattern, () => {
|
|
37
|
+
identityRedactions.push(path);
|
|
38
|
+
return '[participant identity removed]';
|
|
39
|
+
});
|
|
40
|
+
if (scrubbed.length <= maxFieldChars)
|
|
41
|
+
return scrubbed;
|
|
42
|
+
truncations.push(path);
|
|
43
|
+
return `${scrubbed.slice(0, maxFieldChars)}… [truncated by Council]`;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value))
|
|
46
|
+
return value.map((entry, index) => visit(entry, `${path}[${index}]`));
|
|
47
|
+
if (typeof value === 'object' && value !== null) {
|
|
48
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry, `${path}.${key}`)]));
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
value: visit(input, '$'),
|
|
54
|
+
metadata: {
|
|
55
|
+
truncations: [...new Set(truncations)],
|
|
56
|
+
identityRedactions: [...new Set(identityRedactions)],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=anonymize.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Round1Response, Round2Response } from './schemas.js';
|
|
2
|
+
/** Supported Council review styles. */
|
|
3
|
+
export type CouncilStyle = 'balanced' | 'adversarial';
|
|
4
|
+
/** Build a blind first-round prompt. */
|
|
5
|
+
export declare function round1Prompt(question: string): string;
|
|
6
|
+
/** Build an anonymous cross-critique prompt. */
|
|
7
|
+
export declare function round2Prompt(question: string, own: Round1Response, positions: Readonly<Record<string, Round1Response>>, style: CouncilStyle): string;
|
|
8
|
+
/** Build the final revision prompt from anonymous prior artifacts. */
|
|
9
|
+
export declare function round3Prompt(question: string, own: Round1Response, positions: Readonly<Record<string, Round1Response>>, critiques: readonly Round2Response[]): string;
|
|
10
|
+
/** Build the only permitted repair request after invalid structured output. */
|
|
11
|
+
export declare function repairPrompt(originalPrompt: string, raw: string, error: string): string;
|
|
12
|
+
//# sourceMappingURL=prompts.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const READ_ONLY = [
|
|
2
|
+
'This is analytical decision support only.',
|
|
3
|
+
'Do not call tools, edit files, install software, commit, push, deploy, or perform network mutations.',
|
|
4
|
+
'Do not expose private chain-of-thought. Return concise conclusions and reasons in the requested JSON fields.',
|
|
5
|
+
'Return JSON only, without Markdown fences or surrounding prose.',
|
|
6
|
+
].join('\n');
|
|
7
|
+
const ROUND1_SHAPE = `{
|
|
8
|
+
"thesis": "string",
|
|
9
|
+
"recommendation": "string",
|
|
10
|
+
"reasoning": ["concise reason"],
|
|
11
|
+
"assumptions": ["assumption"],
|
|
12
|
+
"risks": ["risk"],
|
|
13
|
+
"evidence_needed": ["evidence"],
|
|
14
|
+
"confidence": 0
|
|
15
|
+
}`;
|
|
16
|
+
const ROUND2_SHAPE = `{
|
|
17
|
+
"critiques": [{
|
|
18
|
+
"target": "P1|P2|P3",
|
|
19
|
+
"strongest_point": "string",
|
|
20
|
+
"weakest_point": "string",
|
|
21
|
+
"possible_error": "string",
|
|
22
|
+
"missing_evidence": "string",
|
|
23
|
+
"severity": "low|medium|high|critical"
|
|
24
|
+
}],
|
|
25
|
+
"preferred_position": "P1|P2|P3|hybrid|undecided",
|
|
26
|
+
"reason": "string",
|
|
27
|
+
"confidence": 0
|
|
28
|
+
}`;
|
|
29
|
+
const ROUND3_SHAPE = `{
|
|
30
|
+
"final_recommendation": "string",
|
|
31
|
+
"preferred_position": "P1|P2|P3|hybrid|undecided",
|
|
32
|
+
"changed_mind": true,
|
|
33
|
+
"change_reason": "string",
|
|
34
|
+
"agreement_points": ["string"],
|
|
35
|
+
"disagreement_points": ["string"],
|
|
36
|
+
"critical_risk": {"active": false, "category": null, "reason": null},
|
|
37
|
+
"confidence": 0
|
|
38
|
+
}`;
|
|
39
|
+
/** Build a blind first-round prompt. */
|
|
40
|
+
export function round1Prompt(question) {
|
|
41
|
+
return [
|
|
42
|
+
READ_ONLY,
|
|
43
|
+
'Independently analyze the question. You have no access to any other participant response.',
|
|
44
|
+
'State a concrete recommendation, its assumptions, risks, and evidence gaps.',
|
|
45
|
+
'Confidence is an integer from 0 through 100.',
|
|
46
|
+
`Question:\n${question}`,
|
|
47
|
+
`Required JSON:\n${ROUND1_SHAPE}`,
|
|
48
|
+
].join('\n\n');
|
|
49
|
+
}
|
|
50
|
+
function reviewInstructions(style) {
|
|
51
|
+
const shared = [
|
|
52
|
+
'Do not seek consensus for its own sake.',
|
|
53
|
+
'If another position is wrong, say why. If your own position is wrong, acknowledge it.',
|
|
54
|
+
'Distinguish factual disagreement from preference disagreement.',
|
|
55
|
+
'Identify missing evidence instead of fabricating certainty.',
|
|
56
|
+
];
|
|
57
|
+
if (style === 'adversarial') {
|
|
58
|
+
shared.push('Identify the strongest argument against each competing position.', 'Prioritize hidden assumptions, unsupported claims, failure modes, contradictory evidence, and implementation risk.', 'Do not reward agreement. Do not manufacture disagreement where none exists.');
|
|
59
|
+
}
|
|
60
|
+
return shared.join('\n');
|
|
61
|
+
}
|
|
62
|
+
/** Build an anonymous cross-critique prompt. */
|
|
63
|
+
export function round2Prompt(question, own, positions, style) {
|
|
64
|
+
return [
|
|
65
|
+
READ_ONLY,
|
|
66
|
+
reviewInstructions(style),
|
|
67
|
+
'Review the anonymous P1/P2/P3 material. These participant labels are unrelated to any option names in the user question.',
|
|
68
|
+
'All three labels are included so preferred_position can use one stable per-run label; focus critique on the two positions that differ from your original answer.',
|
|
69
|
+
'Your original answer is included separately. Do not infer or discuss participant identity.',
|
|
70
|
+
`Question:\n${question}`,
|
|
71
|
+
`Your original answer:\n${JSON.stringify(own)}`,
|
|
72
|
+
`Anonymous positions:\n${JSON.stringify(positions)}`,
|
|
73
|
+
`Required JSON:\n${ROUND2_SHAPE}`,
|
|
74
|
+
].join('\n\n');
|
|
75
|
+
}
|
|
76
|
+
/** Build the final revision prompt from anonymous prior artifacts. */
|
|
77
|
+
export function round3Prompt(question, own, positions, critiques) {
|
|
78
|
+
return [
|
|
79
|
+
READ_ONLY,
|
|
80
|
+
'Update your position only if the evidence or argument justifies it.',
|
|
81
|
+
'You may retain your position, revise it, merge positions, remain undecided, or explicitly dissent.',
|
|
82
|
+
'Do not reach consensus merely because other participants agree.',
|
|
83
|
+
'Set changed_mind accurately and preserve unresolved disagreement.',
|
|
84
|
+
'All three anonymous labels remain present so preferred_position uses the same stable per-run label.',
|
|
85
|
+
`Question:\n${question}`,
|
|
86
|
+
`Your original answer:\n${JSON.stringify(own)}`,
|
|
87
|
+
`Anonymous positions:\n${JSON.stringify(positions)}`,
|
|
88
|
+
`Anonymous critiques:\n${JSON.stringify(critiques)}`,
|
|
89
|
+
`Required JSON:\n${ROUND3_SHAPE}`,
|
|
90
|
+
].join('\n\n');
|
|
91
|
+
}
|
|
92
|
+
/** Build the only permitted repair request after invalid structured output. */
|
|
93
|
+
export function repairPrompt(originalPrompt, raw, error) {
|
|
94
|
+
return [
|
|
95
|
+
READ_ONLY,
|
|
96
|
+
'Your previous response failed JSON validation. Repair only its structure and return one valid JSON object.',
|
|
97
|
+
`Validation error:\n${error}`,
|
|
98
|
+
`Original task:\n${originalPrompt}`,
|
|
99
|
+
`Invalid response:\n${raw}`,
|
|
100
|
+
].join('\n\n');
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { SEAT_NAMES } from '../seats/types.js';
|
|
2
|
+
function bullets(values, empty = 'None identified.') {
|
|
3
|
+
return values.length === 0 ? `- ${empty}` : values.map(value => `- ${value}`).join('\n');
|
|
4
|
+
}
|
|
5
|
+
function responseSections(responses) {
|
|
6
|
+
return ['gpt', 'glm', 'deepseek'].map((seat) => {
|
|
7
|
+
const response = responses.get(seat);
|
|
8
|
+
return response === undefined
|
|
9
|
+
? `## ${SEAT_NAMES[seat]}\n\nUnavailable or failed in this round.`
|
|
10
|
+
: `## ${SEAT_NAMES[seat]}\n\n\`\`\`json\n${JSON.stringify(response.value, null, 2)}\n\`\`\``;
|
|
11
|
+
}).join('\n\n');
|
|
12
|
+
}
|
|
13
|
+
/** Render the complete user-facing Markdown Council report. */
|
|
14
|
+
export function renderReport(result, runId) {
|
|
15
|
+
const verdict = result.verdict;
|
|
16
|
+
const resultLabel = verdict.result.toUpperCase();
|
|
17
|
+
const voteDistribution = Object.entries(verdict.votes).map(([choice, count]) => `${choice}: ${count}`);
|
|
18
|
+
const confidence = ['gpt', 'glm', 'deepseek'].map(seat => (`${SEAT_NAMES[seat]}: ${verdict.confidence[seat] ?? 'unavailable'}`));
|
|
19
|
+
const risks = verdict.criticalRisks.map(risk => (`${SEAT_NAMES[risk.seat]} — ${risk.category}: ${risk.reason}`));
|
|
20
|
+
const mapping = ['gpt', 'glm', 'deepseek'].map(seat => (`| ${SEAT_NAMES[seat]} | ${result.mapping[seat]} |`)).join('\n');
|
|
21
|
+
const failures = verdict.failures.map(failure => (`${SEAT_NAMES[failure.seat]} ${failure.round}: ${failure.error}`));
|
|
22
|
+
const recommendation = verdict.recommendation ?? 'No valid Model Jury recommendation was produced.';
|
|
23
|
+
return `# Model Jury Verdict
|
|
24
|
+
|
|
25
|
+
## Question
|
|
26
|
+
|
|
27
|
+
${result.question}
|
|
28
|
+
|
|
29
|
+
## Result
|
|
30
|
+
|
|
31
|
+
${resultLabel} — quorum ${verdict.quorum}; vote status ${verdict.voteStatus}.
|
|
32
|
+
|
|
33
|
+
## Recommendation
|
|
34
|
+
|
|
35
|
+
${recommendation}
|
|
36
|
+
|
|
37
|
+
${verdict.hybridSemanticCaveat ? '> Multiple seats selected `hybrid`; their textual hybrids are shown separately and are not assumed to be semantically identical.\n' : ''}${verdict.criticalReviewRequired ? '> **CRITICAL REVIEW REQUIRED:** at least two seats independently flagged a compatible critical-risk category.\n' : ''}
|
|
38
|
+
## Consensus
|
|
39
|
+
|
|
40
|
+
${bullets(verdict.consensus, 'No exact shared agreement point was stated by at least two seats.')}
|
|
41
|
+
|
|
42
|
+
## Majority View
|
|
43
|
+
|
|
44
|
+
${bullets(verdict.majorityView, 'No majority was formed.')}
|
|
45
|
+
|
|
46
|
+
## Minority / Dissenting View
|
|
47
|
+
|
|
48
|
+
${bullets(verdict.minorityView, 'No minority view was recorded.')}
|
|
49
|
+
|
|
50
|
+
## Critical Risks
|
|
51
|
+
|
|
52
|
+
${bullets(risks, 'No final position activated a critical-risk flag.')}
|
|
53
|
+
|
|
54
|
+
## Unresolved Questions
|
|
55
|
+
|
|
56
|
+
${bullets(verdict.unresolvedQuestions)}
|
|
57
|
+
|
|
58
|
+
## Evidence Still Needed
|
|
59
|
+
|
|
60
|
+
${bullets(verdict.evidenceStillNeeded)}
|
|
61
|
+
|
|
62
|
+
## Vote Distribution
|
|
63
|
+
|
|
64
|
+
${bullets(voteDistribution)}
|
|
65
|
+
|
|
66
|
+
## Confidence
|
|
67
|
+
|
|
68
|
+
${confidence.join('\n')}
|
|
69
|
+
|
|
70
|
+
## Provider Failures
|
|
71
|
+
|
|
72
|
+
${bullets(failures, 'None.')}
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
# Round 1 — Independent Positions
|
|
77
|
+
|
|
78
|
+
${responseSections(result.round1)}
|
|
79
|
+
|
|
80
|
+
# Round 2 — Anonymous Cross-Critique
|
|
81
|
+
|
|
82
|
+
${responseSections(result.round2)}
|
|
83
|
+
|
|
84
|
+
# Round 3 — Final Positions
|
|
85
|
+
|
|
86
|
+
${responseSections(result.round3)}
|
|
87
|
+
|
|
88
|
+
# Run Metadata
|
|
89
|
+
|
|
90
|
+
- Run ID: ${runId}
|
|
91
|
+
- Style: ${result.style}
|
|
92
|
+
- Successful final seats: ${verdict.successfulSeats.length}/3
|
|
93
|
+
- Changed-mind count: ${verdict.changedMindCount}
|
|
94
|
+
- Critical review required: ${verdict.criticalReviewRequired ? 'yes' : 'no'}
|
|
95
|
+
|
|
96
|
+
| Seat | Anonymous position |
|
|
97
|
+
|---|---|
|
|
98
|
+
${mapping}
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=report.js.map
|