@wyattjoh/demur 0.6.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyattjoh/demur",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "A proof-of-concept destructive-command guard for coding agents.",
6
6
  "license": "MIT",
@@ -39,6 +39,7 @@
39
39
  "src/questions.ts",
40
40
  "src/state.ts",
41
41
  "src/settings-model.ts",
42
+ "src/training-evaluation.ts",
42
43
  "src/training-review-model.ts",
43
44
  "src/training-review-tui.tsx",
44
45
  "src/types.ts"
@@ -1,14 +1,22 @@
1
1
  #!/usr/bin/env bun
2
- import { guard } from "../guard.ts";
2
+ import { guard, guardWithEvidence } from "../guard.ts";
3
3
 
4
4
  type GuardRequest = {
5
5
  command: string;
6
6
  cwd: string;
7
+ includeEvidence: boolean;
7
8
  };
8
9
 
9
10
  const request = parseRequest(await Bun.stdin.text());
10
- const verdict = await guard(request.command, request.cwd, "pi");
11
- process.stdout.write(JSON.stringify(verdict));
11
+ if (request.includeEvidence) {
12
+ const evaluation = await guardWithEvidence(request.command, request.cwd, "pi");
13
+ process.stdout.write(`${JSON.stringify(evaluation.verdict)}\n`);
14
+ process.stdout.write(JSON.stringify(evaluation.evidence ?? null));
15
+ } else {
16
+ process.stdout.write(
17
+ JSON.stringify(await guard(request.command, request.cwd, "pi")),
18
+ );
19
+ }
12
20
 
13
21
  function parseRequest(input: string): GuardRequest {
14
22
  const value: unknown = JSON.parse(input);
@@ -16,10 +24,16 @@ function parseRequest(input: string): GuardRequest {
16
24
  throw new Error("guard request must be an object");
17
25
  }
18
26
 
19
- const { command, cwd } = value as Record<string, unknown>;
20
- if (typeof command !== "string" || typeof cwd !== "string") {
21
- throw new Error("guard request must contain string command and cwd fields");
27
+ const { command, cwd, includeEvidence } = value as Record<string, unknown>;
28
+ if (
29
+ typeof command !== "string" ||
30
+ typeof cwd !== "string" ||
31
+ (includeEvidence !== undefined && typeof includeEvidence !== "boolean")
32
+ ) {
33
+ throw new Error(
34
+ "guard request must contain string command and cwd fields and an optional boolean includeEvidence field",
35
+ );
22
36
  }
23
37
 
24
- return { command, cwd };
38
+ return { command, cwd, includeEvidence: includeEvidence === true };
25
39
  }
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  type DemurSettings,
8
8
  } from "../extensions/demur/settings.ts";
9
9
  import {
10
+ isTrainingCorrectionReason,
10
11
  loadTrainingRecords,
11
12
  loadTrainingReviews,
12
13
  recordTrainingReview,
@@ -15,12 +16,17 @@ import {
15
16
  type TrainingReviewInput,
16
17
  } from "../extensions/demur/training-store.ts";
17
18
  import { guard } from "./guard.ts";
19
+ import { judgeRenderedState, type JudgeResult } from "./judge.ts";
18
20
  import {
19
21
  deleteApiKey,
20
22
  resolveApiKey,
21
23
  storeApiKey,
22
24
  type ResolvedApiKey,
23
25
  } from "./key.ts";
26
+ import {
27
+ analyzeTrainingFeedback,
28
+ replayTrainingQuestions,
29
+ } from "./training-evaluation.ts";
24
30
  import {
25
31
  buildTrainingReviewEntries,
26
32
  createTrainingReviewInput,
@@ -32,7 +38,7 @@ import {
32
38
  type TrainingReviewSnapshot,
33
39
  } from "./training-review-model.ts";
34
40
  import type { TrainingReviewTuiResult } from "./training-review-tui.tsx";
35
- import type { Verdict } from "./types.ts";
41
+ import type { RenderedCommandState, Verdict } from "./types.ts";
36
42
 
37
43
  const USAGE = `Usage:
38
44
  demur
@@ -40,8 +46,9 @@ const USAGE = `Usage:
40
46
  demur auth status
41
47
  demur auth logout
42
48
  demur training list [--status=<all|unreviewed|allow|ask|deny>] [--cwd=<query>] [--json]
49
+ demur training evaluate [--replay] [--limit=<1-100>] [--json]
43
50
  demur training review
44
- demur training review <record-id> --decision=<allow|ask|deny> [--note=<text>] [--json]
51
+ demur training review <record-id> --decision=<allow|ask|deny> [--reason=<correction-reason>] [--note=<text>] [--json]
45
52
  demur judge "<command>" [--cwd=<path>]`;
46
53
 
47
54
  /**
@@ -49,6 +56,7 @@ const USAGE = `Usage:
49
56
  */
50
57
  export type CliDependencies = {
51
58
  judge(command: string, cwd: string): Promise<Verdict>;
59
+ judgeTrainingState(state: RenderedCommandState): Promise<JudgeResult>;
52
60
  resolveApiKey(): Promise<ResolvedApiKey | undefined>;
53
61
  storeApiKey(value: string): Promise<void>;
54
62
  deleteApiKey(): Promise<boolean>;
@@ -74,6 +82,7 @@ export type CliDependencies = {
74
82
 
75
83
  const defaultDependencies: CliDependencies = {
76
84
  judge: (command, cwd) => guard(command, cwd, "cli"),
85
+ judgeTrainingState: judgeRenderedState,
77
86
  resolveApiKey,
78
87
  storeApiKey,
79
88
  deleteApiKey,
@@ -246,6 +255,10 @@ async function runTraining(
246
255
  return runTrainingList(args.slice(1), dependencies);
247
256
  }
248
257
 
258
+ if (command === "evaluate") {
259
+ return runTrainingEvaluate(args.slice(1), dependencies);
260
+ }
261
+
249
262
  if (command === "review") {
250
263
  if (args.length === 1) {
251
264
  return runInteractiveTrainingReview(dependencies);
@@ -256,7 +269,7 @@ async function runTraining(
256
269
  return writeTrainingUsageError(
257
270
  trainingOperation(["training", ...args]),
258
271
  args.includes("--json"),
259
- "expected `training list` or `training review`",
272
+ "expected `training list`, `training evaluate`, or `training review`",
260
273
  dependencies,
261
274
  );
262
275
  }
@@ -370,6 +383,120 @@ async function runTrainingList(
370
383
  return 0;
371
384
  }
372
385
 
386
+ async function runTrainingEvaluate(
387
+ args: ReadonlyArray<string>,
388
+ dependencies: CliDependencies,
389
+ ): Promise<number> {
390
+ const operation = "training.evaluate";
391
+ const parsedResult = parseArguments(
392
+ args,
393
+ new Set(["json", "replay"]),
394
+ new Set(["limit"]),
395
+ );
396
+ const json = args.includes("--json");
397
+ if (parsedResult.error !== undefined) {
398
+ return writeTrainingUsageError(
399
+ operation,
400
+ json,
401
+ parsedResult.error,
402
+ dependencies,
403
+ );
404
+ }
405
+
406
+ const parsed = parsedResult.parsed;
407
+ if (parsed.positionals.length !== 0) {
408
+ return writeTrainingUsageError(
409
+ operation,
410
+ hasOption(parsed, "json"),
411
+ "`training evaluate` does not accept positional arguments",
412
+ dependencies,
413
+ );
414
+ }
415
+
416
+ const limitValue = getStringOption(parsed, "limit");
417
+ if (limitValue !== undefined && !hasOption(parsed, "replay")) {
418
+ return writeTrainingUsageError(
419
+ operation,
420
+ hasOption(parsed, "json"),
421
+ "`--limit` requires `--replay`",
422
+ dependencies,
423
+ );
424
+ }
425
+ const replayLimit = limitValue === undefined ? 20 : Number(limitValue);
426
+ if (
427
+ !Number.isInteger(replayLimit) ||
428
+ replayLimit < 1 ||
429
+ replayLimit > 100
430
+ ) {
431
+ return writeTrainingUsageError(
432
+ operation,
433
+ hasOption(parsed, "json"),
434
+ "`--limit` must be an integer from 1 to 100",
435
+ dependencies,
436
+ );
437
+ }
438
+
439
+ const entries = await loadTrainingEntries(dependencies);
440
+ const report = analyzeTrainingFeedback(entries);
441
+ const replay = hasOption(parsed, "replay")
442
+ ? await replayTrainingQuestions(
443
+ entries,
444
+ dependencies.judgeTrainingState,
445
+ replayLimit,
446
+ )
447
+ : undefined;
448
+ if (hasOption(parsed, "json")) {
449
+ writeTrainingJsonSuccess(
450
+ operation,
451
+ replay === undefined ? report : { offline: report, replay },
452
+ dependencies,
453
+ );
454
+ return 0;
455
+ }
456
+
457
+ dependencies.stdout(
458
+ `Reviewed ${report.reviewed}; evaluable ${report.evaluable}; unavailable ${report.unavailable}.`,
459
+ );
460
+ dependencies.stdout(
461
+ `Replay fidelity: ${report.completeReplayRecords} complete, ${report.policyOnlyRecords} policy-only legacy.`,
462
+ );
463
+ dependencies.stdout(
464
+ `Current policy: ${report.current.matches}/${report.current.evaluated} match; weighted loss ${report.current.weightedLoss}.`,
465
+ );
466
+
467
+ const reasons = Object.entries(report.correctionsByReason);
468
+ if (reasons.length > 0) {
469
+ dependencies.stdout(
470
+ `Correction reasons: ${reasons.map(([reason, count]) => `${reason}=${count}`).join(", ")}.`,
471
+ );
472
+ }
473
+
474
+ if (report.candidates.length === 0) {
475
+ dependencies.stdout("No single-threshold candidate improved observed weighted loss.");
476
+ } else {
477
+ dependencies.stdout("Exploratory single-threshold candidates:");
478
+ for (const candidate of report.candidates) {
479
+ dependencies.stdout(
480
+ ` ${candidate.field}=${candidate.value}: ${candidate.metrics.matches}/${candidate.metrics.evaluated} match; weighted loss ${candidate.metrics.weightedLoss}`,
481
+ );
482
+ }
483
+ }
484
+
485
+ if (report.warning !== undefined) dependencies.stdout(`Warning: ${report.warning}`);
486
+ dependencies.stdout(
487
+ "Candidates are not applied automatically; validate them on an independent holdout and the synthetic corpus.",
488
+ );
489
+ if (replay !== undefined) {
490
+ dependencies.stdout(
491
+ `Current questions replay: ${replay.metrics.matches}/${replay.metrics.evaluated} match; ${replay.improved} improved; ${replay.regressed} regressed; ${replay.unavailable} unavailable; ${replay.skipped} skipped by limit.`,
492
+ );
493
+ dependencies.stdout(
494
+ `Replay usage: ${replay.inputTokens} input / ${replay.outputTokens} output tokens. Stored commands remained data and were never executed.`,
495
+ );
496
+ }
497
+ return 0;
498
+ }
499
+
373
500
  async function runTrainingReview(
374
501
  args: ReadonlyArray<string>,
375
502
  dependencies: CliDependencies,
@@ -378,7 +505,7 @@ async function runTrainingReview(
378
505
  const parsedResult = parseArguments(
379
506
  args,
380
507
  new Set(["json"]),
381
- new Set(["decision", "note"]),
508
+ new Set(["decision", "reason", "note"]),
382
509
  );
383
510
  const json = args.includes("--json");
384
511
  if (parsedResult.error !== undefined) {
@@ -423,15 +550,37 @@ async function runTrainingReview(
423
550
  );
424
551
  }
425
552
 
553
+ const corrected = decisionValue !== entry.record.verdict.decision;
554
+ const reasonValue = getStringOption(parsed, "reason");
555
+ if (corrected && !isTrainingCorrectionReason(reasonValue)) {
556
+ return writeTrainingUsageError(
557
+ operation,
558
+ hasOption(parsed, "json"),
559
+ "corrected reviews require `--reason` with a supported correction reason",
560
+ dependencies,
561
+ );
562
+ }
563
+ if (!corrected && reasonValue !== undefined) {
564
+ return writeTrainingUsageError(
565
+ operation,
566
+ hasOption(parsed, "json"),
567
+ "`--reason` is only valid when correcting the model decision",
568
+ dependencies,
569
+ );
570
+ }
571
+
572
+ const correctionReason = corrected && isTrainingCorrectionReason(reasonValue)
573
+ ? reasonValue
574
+ : undefined;
426
575
  const previousReview = getLatestTrainingReview(entry) ?? null;
427
576
  const review = await dependencies.recordTrainingReview(
428
577
  createTrainingReviewInput(
429
578
  entry.record,
430
579
  decisionValue,
580
+ correctionReason,
431
581
  getStringOption(parsed, "note"),
432
582
  ),
433
583
  );
434
- const corrected = decisionValue !== entry.record.verdict.decision;
435
584
 
436
585
  if (hasOption(parsed, "json")) {
437
586
  writeTrainingJsonSuccess(
@@ -6,14 +6,91 @@ import {
6
6
  Predicate,
7
7
  Result,
8
8
  } from "effect";
9
- import { judgeEffect, Judgment } from "./judge.ts";
9
+ import { judgeEffect, Judgment, TYPESAFE_MODEL } from "./judge.ts";
10
10
  import { Environment } from "./key.ts";
11
- import { applyStaticGate, decide } from "./policy.ts";
12
- import { gatherStateEffect, GitCommand } from "./state.ts";
13
- import type { CommandState, Host, Verdict } from "./types.ts";
11
+ import {
12
+ applyStaticGate,
13
+ decide,
14
+ POLICY_VERSION,
15
+ THRESHOLDS,
16
+ } from "./policy.ts";
17
+ import { QUESTION_SET_VERSION } from "./questions.ts";
18
+ import { gatherStateEffect, GitCommand, renderState } from "./state.ts";
19
+ import type {
20
+ CommandState,
21
+ GuardEvaluation,
22
+ Host,
23
+ Verdict,
24
+ } from "./types.ts";
14
25
 
15
26
  const PREFIX = "demur:";
16
27
 
28
+ /**
29
+ * Effect-native guard implementation with replayable evidence.
30
+ *
31
+ * @param command - The shell command the agent wants to run
32
+ * @param cwd - Absolute working directory for the command
33
+ * @param agent - Which coding agent is asking
34
+ * @returns A fail-closed verdict and the exact evidence behind it
35
+ */
36
+ export const guardEvaluationEffect = Effect.fn("guardEvaluationEffect")(
37
+ function* (
38
+ command: string,
39
+ cwd: string,
40
+ agent: Host,
41
+ ): Effect.fn.Return<
42
+ GuardEvaluation,
43
+ never,
44
+ Environment | GitCommand | Judgment
45
+ > {
46
+ const started = yield* Clock.monotonicTimeNanos;
47
+ const core = Effect.gen(function* () {
48
+ const environment = yield* Environment;
49
+ const disabled = isDisabled(yield* environment.get("DEMUR_DISABLE"));
50
+
51
+ if (disabled) {
52
+ const verdict = yield* completeVerdict(started, {
53
+ ...emptyEvidence,
54
+ decision: "allow",
55
+ reason: `${PREFIX} disabled via DEMUR_DISABLE.`,
56
+ });
57
+ return { verdict, evidence: undefined } satisfies GuardEvaluation;
58
+ }
59
+
60
+ if (command.trim() === "") {
61
+ const verdict = yield* completeVerdict(started, {
62
+ ...emptyEvidence,
63
+ decision: "allow",
64
+ reason: `${PREFIX} empty command.`,
65
+ });
66
+ return { verdict, evidence: undefined } satisfies GuardEvaluation;
67
+ }
68
+
69
+ const state = yield* gatherStateEffect(command, cwd, agent);
70
+ const verdict = yield* judgeStateCore(state, started, 0);
71
+ return {
72
+ verdict,
73
+ evidence: {
74
+ modelState: renderState(state),
75
+ analysis: state.analysis,
76
+ model: TYPESAFE_MODEL,
77
+ questionSetVersion: QUESTION_SET_VERSION,
78
+ policyVersion: POLICY_VERSION,
79
+ policyThresholds: { ...THRESHOLDS },
80
+ },
81
+ } satisfies GuardEvaluation;
82
+ });
83
+
84
+ const exit = yield* Effect.exit(core);
85
+ if (Exit.isSuccess(exit)) return exit.value;
86
+
87
+ return {
88
+ verdict: yield* unexpectedVerdict(started, exit.cause, 0),
89
+ evidence: undefined,
90
+ };
91
+ },
92
+ );
93
+
17
94
  /**
18
95
  * Effect-native guard implementation used by the Promise boundary.
19
96
  *
@@ -27,35 +104,8 @@ export const guardEffect = Effect.fn("guardEffect")(function* (
27
104
  cwd: string,
28
105
  agent: Host,
29
106
  ): Effect.fn.Return<Verdict, never, Environment | GitCommand | Judgment> {
30
- const started = yield* Clock.monotonicTimeNanos;
31
- const core = Effect.gen(function* () {
32
- const environment = yield* Environment;
33
- const disabled = isDisabled(yield* environment.get("DEMUR_DISABLE"));
34
-
35
- if (disabled) {
36
- return yield* completeVerdict(started, {
37
- ...emptyEvidence,
38
- decision: "allow",
39
- reason: `${PREFIX} disabled via DEMUR_DISABLE.`,
40
- });
41
- }
42
-
43
- if (command.trim() === "") {
44
- return yield* completeVerdict(started, {
45
- ...emptyEvidence,
46
- decision: "allow",
47
- reason: `${PREFIX} empty command.`,
48
- });
49
- }
50
-
51
- const state = yield* gatherStateEffect(command, cwd, agent);
52
- return yield* judgeStateCore(state, started, 0);
53
- });
54
-
55
- const exit = yield* Effect.exit(core);
56
- if (Exit.isSuccess(exit)) return exit.value;
57
-
58
- return yield* unexpectedVerdict(started, exit.cause, 0);
107
+ const evaluation = yield* guardEvaluationEffect(command, cwd, agent);
108
+ return evaluation.verdict;
59
109
  });
60
110
 
61
111
  /**
package/src/guard.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  import { Layer, ManagedRuntime, Predicate } from "effect";
2
- import { guardEffect, judgeStateEffect } from "./guard.internal.ts";
2
+ import {
3
+ guardEffect,
4
+ guardEvaluationEffect,
5
+ judgeStateEffect,
6
+ } from "./guard.internal.ts";
3
7
  import { Judgment } from "./judge.ts";
4
8
  import { Environment } from "./key.ts";
5
9
  import { GitCommand } from "./state.ts";
6
- import type { CommandState, Host, Verdict } from "./types.ts";
10
+ import type {
11
+ CommandState,
12
+ GuardEvaluation,
13
+ Host,
14
+ Verdict,
15
+ } from "./types.ts";
7
16
 
8
17
  const PREFIX = "demur:";
9
18
 
@@ -41,6 +50,37 @@ export function guard(
41
50
  .catch((error: unknown) => unexpectedVerdict(error, started));
42
51
  }
43
52
 
53
+ /**
54
+ * Judge one command while retaining the exact state and policy versions.
55
+ *
56
+ * This boundary is used only when a host has enabled local training capture;
57
+ * ordinary guard callers continue to receive the smaller {@link Verdict}.
58
+ *
59
+ * @param command - The shell command the agent wants to run
60
+ * @param cwd - Absolute working directory for the command
61
+ * @param agent - Which coding agent is asking
62
+ * @param signal - Optional cancellation signal from the host
63
+ * @returns The verdict and replayable evidence, when state collection occurred
64
+ */
65
+ export function guardWithEvidence(
66
+ command: string,
67
+ cwd: string,
68
+ agent: Host,
69
+ signal: AbortSignal | undefined = undefined,
70
+ ): Promise<GuardEvaluation> {
71
+ const started = performance.now();
72
+
73
+ return runtime
74
+ .runPromise(
75
+ guardEvaluationEffect(command, cwd, agent),
76
+ signal === undefined ? undefined : { signal },
77
+ )
78
+ .catch((error: unknown) => ({
79
+ verdict: unexpectedVerdict(error, started),
80
+ evidence: undefined,
81
+ }));
82
+ }
83
+
44
84
  /**
45
85
  * Judge a command from state that has already been collected.
46
86
  *
package/src/judge.ts CHANGED
@@ -19,7 +19,12 @@ import {
19
19
  } from "./key.ts";
20
20
  import { COMMAND_JUDGMENTS } from "./questions.ts";
21
21
  import { renderState } from "./state.ts";
22
- import type { CommandState, FailureKind, Judgments } from "./types.ts";
22
+ import type {
23
+ CommandState,
24
+ FailureKind,
25
+ Judgments,
26
+ RenderedCommandState,
27
+ } from "./types.ts";
23
28
 
24
29
  /**
25
30
  * Default per-attempt timeout for the judgment call.
@@ -29,7 +34,10 @@ import type { CommandState, FailureKind, Judgments } from "./types.ts";
29
34
  */
30
35
  const DEFAULT_TIMEOUT_MS = 4000;
31
36
 
32
- const TYPESAFE_MODEL = "jev-latest";
37
+ /**
38
+ * TypeSafe model used for command judgments.
39
+ */
40
+ export const TYPESAFE_MODEL = "jev-latest";
33
41
 
34
42
  /**
35
43
  * Build the TypeSafe-backed Effect decision model for one API key.
@@ -90,7 +98,9 @@ export class JudgmentError extends Schema.TaggedError<JudgmentError>()(
90
98
  export class Judgment extends Context.Service<
91
99
  Judgment,
92
100
  {
93
- judge(state: CommandState): Effect.Effect<JudgeSuccess, JudgmentError>;
101
+ judge(
102
+ state: RenderedCommandState,
103
+ ): Effect.Effect<JudgeSuccess, JudgmentError>;
94
104
  }
95
105
  >()("demur/judge/Judgment") {
96
106
  static readonly layerNoDeps = Layer.effect(
@@ -134,11 +144,11 @@ export class Judgment extends Context.Service<
134
144
  );
135
145
 
136
146
  const judge = Effect.fn("Judgment.judge")(function* (
137
- state: CommandState,
147
+ state: RenderedCommandState,
138
148
  ): Effect.fn.Return<JudgeSuccess, JudgmentError> {
139
149
  const activeDecisionLayer = yield* getDecisionLayer();
140
150
  const result = yield* DecisionModel.decide(COMMAND_JUDGMENTS, {
141
- input: renderState(state),
151
+ input: state,
142
152
  }).pipe(
143
153
  Effect.provide(activeDecisionLayer),
144
154
  Effect.timeout(timeoutMs),
@@ -188,9 +198,24 @@ export const judgeEffect = Effect.fn("judgeEffect")(function* (
188
198
  state: CommandState,
189
199
  ): Effect.fn.Return<JudgeSuccess, JudgmentError, Judgment> {
190
200
  const judgment = yield* Judgment;
191
- return yield* judgment.judge(state);
201
+ return yield* judgment.judge(renderState(state));
192
202
  });
193
203
 
204
+ /**
205
+ * Ask the configured judgment service about already-rendered TypeSafe state.
206
+ *
207
+ * @param state - Exact JSON state captured from an earlier invocation
208
+ * @returns The current question set's judgments in the Effect error channel
209
+ */
210
+ export const judgeRenderedStateEffect = Effect.fn("judgeRenderedStateEffect")(
211
+ function* (
212
+ state: RenderedCommandState,
213
+ ): Effect.fn.Return<JudgeSuccess, JudgmentError, Judgment> {
214
+ const judgment = yield* Judgment;
215
+ return yield* judgment.judge(state);
216
+ },
217
+ );
218
+
194
219
  const runtime = ManagedRuntime.make(Judgment.layer);
195
220
 
196
221
  /**
@@ -206,9 +231,30 @@ const runtime = ManagedRuntime.make(Judgment.layer);
206
231
  export function judge(
207
232
  state: CommandState,
208
233
  signal: AbortSignal | undefined = undefined,
234
+ ): Promise<JudgeResult> {
235
+ return runJudgment(judgeEffect(state), signal);
236
+ }
237
+
238
+ /**
239
+ * Re-run the current question set against exact previously captured state.
240
+ *
241
+ * @param state - Exact model-visible state from a version-two training record
242
+ * @param signal - Optional cancellation signal
243
+ * @returns Current raw judgments or a typed failure
244
+ */
245
+ export function judgeRenderedState(
246
+ state: RenderedCommandState,
247
+ signal: AbortSignal | undefined = undefined,
248
+ ): Promise<JudgeResult> {
249
+ return runJudgment(judgeRenderedStateEffect(state), signal);
250
+ }
251
+
252
+ function runJudgment(
253
+ effect: Effect.Effect<JudgeSuccess, JudgmentError, Judgment>,
254
+ signal: AbortSignal | undefined,
209
255
  ): Promise<JudgeResult> {
210
256
  const program = Effect.gen(function* () {
211
- const result = yield* Effect.result(judgeEffect(state));
257
+ const result = yield* Effect.result(effect);
212
258
  if (Result.isFailure(result)) {
213
259
  return {
214
260
  ok: false,
package/src/policy.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  import type { CommandAnalysis } from "./analyze.ts";
2
2
  import type { Decision, Judgments } from "./types.ts";
3
3
 
4
+ /**
5
+ * Version of the policy ordering and threshold semantics.
6
+ *
7
+ * Increment this whenever composition behavior changes. Threshold values are
8
+ * captured separately by offline evaluation, so candidate sweeps do not need a
9
+ * version bump until a candidate is promoted.
10
+ */
11
+ export const POLICY_VERSION = 2;
12
+
4
13
  /**
5
14
  * Decision thresholds applied to raw judgments.
6
15
  *
@@ -81,7 +90,7 @@ export const THRESHOLDS: Thresholds = {
81
90
  denyUnrecoverable: 0.45,
82
91
  denySharedInfrastructure: 0.5,
83
92
  denyBlastRadius: 2.0,
84
- minBlastRadiusConfidence: 0.5,
93
+ minBlastRadiusConfidence: 0.95,
85
94
  uncertaintyBand: 0.04,
86
95
  uncertaintyBandScore: 0.12,
87
96
  staticGateMinExecution: 0.8,
package/src/questions.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { Schema } from "effect";
2
2
  import { Decision } from "effect/unstable/ai";
3
3
 
4
+ /**
5
+ * Version of the complete TypeSafe question set.
6
+ *
7
+ * Increment this whenever question instructions, criteria, or primitives change
8
+ * so captured training evidence remains attributable to its exact semantics.
9
+ */
10
+ export const QUESTION_SET_VERSION = 2;
11
+
4
12
  /**
5
13
  * The judgment set demur asks about every command.
6
14
  *
@@ -35,7 +43,7 @@ export const QUESTIONS = {
35
43
  criteria: {
36
44
  true: "It reads a value from an environment variable, file, credential or secret store, database, or remote system and prints, logs, copies, or transmits it. Read-only output still counts.",
37
45
  false:
38
- "It reveals only names, metadata, schema, attributes, or redacted values; rotates or revokes without revealing; or quotes or prints secret-looking names or commands as documentation, comments, or other inert payload.",
46
+ "It reveals only names, metadata, schema, attributes, or redacted values; looks up or describes a credential item without requesting its secret value; rotates or revokes without revealing; or quotes or prints secret-looking names or commands as documentation, comments, or other inert payload. For example, `security find-generic-password -s NAME` without `-w` prints item attributes rather than the password value.",
39
47
  },
40
48
  }),
41
49