create-quorum-router 0.1.11 → 0.1.13

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/README.md CHANGED
@@ -17,7 +17,7 @@ deno task intake
17
17
  deno task supabase:status
18
18
  ```
19
19
 
20
- Current package version: `create-quorum-router@0.1.11`. Releases are published
20
+ Current package version: `create-quorum-router@0.1.13`. Releases are published
21
21
  from an immutable Git tag through GitHub Actions OIDC Trusted Publishing.
22
22
 
23
23
  ## What the generated project supports
@@ -25,11 +25,14 @@ from an immutable Git tag through GitHub Actions OIDC Trusted Publishing.
25
25
  `deno task smoke` is deterministic fixture-only and does not call a provider
26
26
  API.
27
27
 
28
- `deno task calibration:demo` runs the bundled calibration-by-task API against
29
- deterministic local observations. The report is advisory-only and is not
30
- connected to routing weights, provider eligibility, or execution. The command
31
- does not call provider APIs; on a new Deno installation, its first run resolves
32
- the pinned Zod dependency before execution.
28
+ `deno task calibration:demo` runs the bundled flat calibration-by-task API
29
+ against deterministic local observations. For narrower diagnostics, generated
30
+ projects also export `aggregateHierarchicalTaskCalibration()` and
31
+ `resolveHierarchicalTaskCalibration()` with
32
+ `prompt_pattern task_subtype task_type` sample-count fallback. Reports are
33
+ advisory-only and are not connected to routing weights, provider eligibility, or
34
+ execution. The command does not call provider APIs; on a new Deno installation,
35
+ its first run resolves the pinned Zod dependency before execution.
33
36
 
34
37
  `deno task intake` is the first real setup command. It detects local provider
35
38
  wrappers, checks OAuth/session status, runs safe list-only model inventory where
@@ -3,7 +3,7 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
  const { spawnSync } = require("child_process");
5
5
 
6
- const VERSION = "0.1.11";
6
+ const VERSION = "0.1.13";
7
7
  const SUPPORTED_TEMPLATES = new Set(["basic"]);
8
8
 
9
9
  function usage() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-quorum-router",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Create a local QuorumRouter project.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  # QuorumRouter generated workspace
2
2
 
3
3
  This generated workspace contains the MIT-licensed QuorumRouter current release.
4
- npm latest targets v0.1.11.
4
+ npm latest targets v0.1.13.
5
5
 
6
6
  QuorumRouter is **MIT**. It is **open source**. Commercial and production use
7
7
  are permitted under the MIT License.
@@ -54,6 +54,15 @@ not use them to change routing weights, ranks, provider eligibility, quorum, or
54
54
  execution. The command does not call provider APIs; on a new Deno installation,
55
55
  its first run resolves the pinned Zod dependency before execution.
56
56
 
57
+ For narrower diagnostics, import `aggregateHierarchicalTaskCalibration()` and
58
+ `resolveHierarchicalTaskCalibration()` from `src/calibration.ts`. For example,
59
+ classify an observation with `task_type: "code_review"`,
60
+ `task_subtype: "typescript"`, and `prompt_pattern: "schema-boundary-review"`.
61
+ Resolution checks `prompt_pattern → task_subtype → task_type`, using the first
62
+ group that reaches the configured sample threshold. Groups never cross exact
63
+ provider/model source boundaries, labels must not contain raw prompts, and the
64
+ result remains advisory-only.
65
+
57
66
  `intake` detects local provider wrappers, checks OAuth/session status, runs safe
58
67
  model inventory/list-only probes where possible, writes local health traces
59
68
  under `out/`, and recommends the next command.
@@ -228,8 +237,9 @@ not paste them into chat/logs and do not commit `.env`.
228
237
  `src/provider_client.ts` — provider discovery and safe invocation.
229
238
  - `src/best_route.ts`, `src/agent_chat.ts` — gated dogfood commands.
230
239
  - `src/cost_aware.ts` — estimated-cost budget selection for Best Route.
231
- - `src/calibration.ts`, `src/calibration_demo.ts` — strict advisory calibration
232
- aggregation and an offline runnable example.
240
+ - `src/calibration.ts`, `src/calibration_demo.ts` — strict flat and hierarchical
241
+ advisory aggregation, parent fallback resolution, and an offline runnable
242
+ example.
233
243
  - `src/trace.ts`, `src/redact.ts`, `src/schema.ts`, `src/fixture_smoke.ts` —
234
244
  trace/redaction/schema/fixture support.
235
245
  - `out/.gitkeep` — local output directory placeholder.
@@ -28,6 +28,10 @@ const ObservationIdSchema = boundedCanonicalText(
28
28
  MAX_CALIBRATION_IDENTIFIER_LENGTH,
29
29
  );
30
30
  const TaskTypeSchema = boundedCanonicalText(MAX_CALIBRATION_IDENTIFIER_LENGTH);
31
+ const HierarchicalTaxonomyLabelSchema = boundedCanonicalText(128).refine(
32
+ (value) => /^[a-z0-9](?:[a-z0-9._:/-]{0,126}[a-z0-9])?$/.test(value),
33
+ "hierarchical taxonomy labels must be lowercase canonical ASCII identifiers",
34
+ );
31
35
  const SourceLabelSchema = boundedCanonicalText(
32
36
  MAX_CALIBRATION_IDENTIFIER_LENGTH,
33
37
  );
@@ -247,3 +251,497 @@ export function aggregateTaskCalibration(
247
251
  groups,
248
252
  });
249
253
  }
254
+
255
+ export const MAX_HIERARCHICAL_CALIBRATION_GROUPS =
256
+ MAX_CALIBRATION_OBSERVATIONS * 3;
257
+
258
+ export const HierarchicalCalibrationScopeSchema = z.enum([
259
+ "task_type",
260
+ "task_subtype",
261
+ "prompt_pattern",
262
+ ]);
263
+
264
+ export const HierarchicalTaskCalibrationObservationSchema = z.object({
265
+ observation_id: ObservationIdSchema,
266
+ task_type: TaskTypeSchema,
267
+ task_subtype: HierarchicalTaxonomyLabelSchema.optional(),
268
+ prompt_pattern: HierarchicalTaxonomyLabelSchema.optional(),
269
+ source: TaskCalibrationSourceSchema,
270
+ evaluation_basis: z.literal("caller_attested_external_ground_truth"),
271
+ correct: z.boolean(),
272
+ confidence: z.number().finite().min(0).max(1),
273
+ evaluated_at: EvaluationTimestampSchema,
274
+ }).strict().superRefine((observation, context) => {
275
+ if (
276
+ observation.prompt_pattern !== undefined &&
277
+ observation.task_subtype === undefined
278
+ ) {
279
+ context.addIssue({
280
+ code: "custom",
281
+ message: "prompt_pattern requires task_subtype",
282
+ path: ["prompt_pattern"],
283
+ });
284
+ }
285
+ });
286
+
287
+ export const HierarchicalTaskCalibrationObservationListSchema = z.array(
288
+ HierarchicalTaskCalibrationObservationSchema,
289
+ ).max(MAX_CALIBRATION_OBSERVATIONS).superRefine((observations, context) => {
290
+ const ids = new Set<string>();
291
+ for (const [index, observation] of observations.entries()) {
292
+ if (ids.has(observation.observation_id)) {
293
+ context.addIssue({
294
+ code: "custom",
295
+ message: "observation_id must be unique",
296
+ path: [index, "observation_id"],
297
+ });
298
+ }
299
+ ids.add(observation.observation_id);
300
+ }
301
+ });
302
+
303
+ export const HierarchicalTaskCalibrationGroupSchema = z.object({
304
+ scope: HierarchicalCalibrationScopeSchema,
305
+ task_type: TaskTypeSchema,
306
+ task_subtype: HierarchicalTaxonomyLabelSchema.optional(),
307
+ prompt_pattern: HierarchicalTaxonomyLabelSchema.optional(),
308
+ source: TaskCalibrationSourceSchema,
309
+ sample_count: z.number().int().positive().max(MAX_CALIBRATION_OBSERVATIONS),
310
+ accuracy: z.number().finite().min(0).max(1),
311
+ mean_confidence: z.number().finite().min(0).max(1),
312
+ brier_score: z.number().finite().min(0).max(1),
313
+ mean_calibration_bias: z.number().finite().min(-1).max(1),
314
+ sample_status: z.enum(["insufficient", "sufficient"]),
315
+ }).strict().superRefine((group, context) => {
316
+ const validLabels =
317
+ (group.scope === "task_type" && group.task_subtype === undefined &&
318
+ group.prompt_pattern === undefined) ||
319
+ (group.scope === "task_subtype" && group.task_subtype !== undefined &&
320
+ group.prompt_pattern === undefined) ||
321
+ (group.scope === "prompt_pattern" && group.task_subtype !== undefined &&
322
+ group.prompt_pattern !== undefined);
323
+ if (!validLabels) {
324
+ context.addIssue({
325
+ code: "custom",
326
+ message: "scope must match task_subtype and prompt_pattern labels",
327
+ });
328
+ }
329
+ });
330
+
331
+ export const HierarchicalTaskCalibrationReportSchema = z.object({
332
+ schema_version: z.literal("quorum-router.hierarchical-calibration.v1"),
333
+ advisory_only: z.literal(true),
334
+ minimum_sample_count: z.number().int().positive().max(
335
+ MAX_CALIBRATION_OBSERVATIONS,
336
+ ),
337
+ groups: z.array(HierarchicalTaskCalibrationGroupSchema).max(
338
+ MAX_HIERARCHICAL_CALIBRATION_GROUPS,
339
+ ),
340
+ }).strict().superRefine((report, context) => {
341
+ const groupKeys = new Set<string>();
342
+ for (const [index, group] of report.groups.entries()) {
343
+ const groupKey = JSON.stringify([
344
+ group.scope,
345
+ group.task_type,
346
+ group.task_subtype ?? null,
347
+ group.prompt_pattern ?? null,
348
+ group.source.provider,
349
+ group.source.model,
350
+ ]);
351
+ if (groupKeys.has(groupKey)) {
352
+ context.addIssue({
353
+ code: "custom",
354
+ message: "hierarchical calibration report groups must be unique",
355
+ path: ["groups", index],
356
+ });
357
+ }
358
+ groupKeys.add(groupKey);
359
+
360
+ const expectedStatus = group.sample_count < report.minimum_sample_count
361
+ ? "insufficient"
362
+ : "sufficient";
363
+ if (group.sample_status !== expectedStatus) {
364
+ context.addIssue({
365
+ code: "custom",
366
+ message: "sample_status must match the configured sample threshold",
367
+ path: ["groups", index, "sample_status"],
368
+ });
369
+ }
370
+ if (
371
+ group.mean_calibration_bias !==
372
+ stableMetric(group.mean_confidence - group.accuracy)
373
+ ) {
374
+ context.addIssue({
375
+ code: "custom",
376
+ message: "mean_calibration_bias must equal mean_confidence - accuracy",
377
+ path: ["groups", index, "mean_calibration_bias"],
378
+ });
379
+ }
380
+ }
381
+ });
382
+
383
+ export const HierarchicalTaskCalibrationQuerySchema = z.object({
384
+ task_type: TaskTypeSchema,
385
+ task_subtype: HierarchicalTaxonomyLabelSchema.optional(),
386
+ prompt_pattern: HierarchicalTaxonomyLabelSchema.optional(),
387
+ source: TaskCalibrationSourceSchema,
388
+ }).strict().superRefine((query, context) => {
389
+ if (query.prompt_pattern !== undefined && query.task_subtype === undefined) {
390
+ context.addIssue({
391
+ code: "custom",
392
+ message: "prompt_pattern requires task_subtype",
393
+ path: ["prompt_pattern"],
394
+ });
395
+ }
396
+ });
397
+
398
+ const HierarchicalCalibrationCandidateSchema = z.object({
399
+ scope: HierarchicalCalibrationScopeSchema,
400
+ sample_count: z.number().int().positive().max(MAX_CALIBRATION_OBSERVATIONS)
401
+ .nullable(),
402
+ sample_status: z.enum(["missing", "insufficient", "sufficient"]),
403
+ }).strict().superRefine((candidate, context) => {
404
+ if (
405
+ (candidate.sample_status === "missing") !==
406
+ (candidate.sample_count === null)
407
+ ) {
408
+ context.addIssue({
409
+ code: "custom",
410
+ message: "missing candidates must have a null sample_count",
411
+ });
412
+ }
413
+ });
414
+
415
+ export const HierarchicalTaskCalibrationSelectionSchema = z.object({
416
+ schema_version: z.literal("quorum-router.hierarchical-selection.v1"),
417
+ advisory_only: z.literal(true),
418
+ query: HierarchicalTaskCalibrationQuerySchema,
419
+ requested_scope: HierarchicalCalibrationScopeSchema,
420
+ resolution_status: z.enum(["selected", "no_sufficient_group"]),
421
+ selected_scope: HierarchicalCalibrationScopeSchema.nullable(),
422
+ candidates: z.array(HierarchicalCalibrationCandidateSchema).min(1).max(3),
423
+ selected_group: HierarchicalTaskCalibrationGroupSchema.optional(),
424
+ }).strict().superRefine((selection, context) => {
425
+ const queryScope = selection.query.prompt_pattern !== undefined
426
+ ? "prompt_pattern"
427
+ : selection.query.task_subtype !== undefined
428
+ ? "task_subtype"
429
+ : "task_type";
430
+ if (selection.requested_scope !== queryScope) {
431
+ context.addIssue({
432
+ code: "custom",
433
+ message: "requested_scope must match the query labels",
434
+ path: ["requested_scope"],
435
+ });
436
+ }
437
+
438
+ const expectedScopes = selection.requested_scope === "prompt_pattern"
439
+ ? ["prompt_pattern", "task_subtype", "task_type"]
440
+ : selection.requested_scope === "task_subtype"
441
+ ? ["task_subtype", "task_type"]
442
+ : ["task_type"];
443
+ if (
444
+ JSON.stringify(selection.candidates.map((candidate) => candidate.scope)) !==
445
+ JSON.stringify(expectedScopes)
446
+ ) {
447
+ context.addIssue({
448
+ code: "custom",
449
+ message: "candidates must follow the requested scope fallback order",
450
+ path: ["candidates"],
451
+ });
452
+ }
453
+
454
+ const firstSufficient = selection.candidates.find((candidate) =>
455
+ candidate.sample_status === "sufficient"
456
+ );
457
+ if (firstSufficient === undefined) {
458
+ if (
459
+ selection.resolution_status !== "no_sufficient_group" ||
460
+ selection.selected_scope !== null ||
461
+ selection.selected_group !== undefined
462
+ ) {
463
+ context.addIssue({
464
+ code: "custom",
465
+ message: "no sufficient candidate must produce no selection",
466
+ });
467
+ }
468
+ } else if (
469
+ selection.resolution_status !== "selected" ||
470
+ selection.selected_scope !== firstSufficient.scope ||
471
+ selection.selected_group?.scope !== firstSufficient.scope ||
472
+ selection.selected_group.sample_count !== firstSufficient.sample_count ||
473
+ selection.selected_group.sample_status !== "sufficient" ||
474
+ selection.selected_group.task_type !== selection.query.task_type ||
475
+ selection.selected_group.source.provider !==
476
+ selection.query.source.provider ||
477
+ selection.selected_group.source.model !== selection.query.source.model ||
478
+ (firstSufficient.scope !== "task_type" &&
479
+ selection.selected_group.task_subtype !== selection.query.task_subtype) ||
480
+ (firstSufficient.scope === "prompt_pattern" &&
481
+ selection.selected_group.prompt_pattern !==
482
+ selection.query.prompt_pattern)
483
+ ) {
484
+ context.addIssue({
485
+ code: "custom",
486
+ message: "selection must use the first sufficient candidate",
487
+ });
488
+ }
489
+ });
490
+
491
+ export const HierarchicalTaskCalibrationDecisionSchema = z.object({
492
+ report: HierarchicalTaskCalibrationReportSchema,
493
+ selection: HierarchicalTaskCalibrationSelectionSchema,
494
+ }).strict().superRefine((decision, context) => {
495
+ const { query } = decision.selection;
496
+ const expectedGroups = decision.selection.candidates.map((candidate) =>
497
+ decision.report.groups.find((group) =>
498
+ group.scope === candidate.scope && group.task_type === query.task_type &&
499
+ group.source.provider === query.source.provider &&
500
+ group.source.model === query.source.model &&
501
+ (candidate.scope === "task_type" ||
502
+ group.task_subtype === query.task_subtype) &&
503
+ (candidate.scope !== "prompt_pattern" ||
504
+ group.prompt_pattern === query.prompt_pattern)
505
+ )
506
+ );
507
+
508
+ decision.selection.candidates.forEach((candidate, index) => {
509
+ const group = expectedGroups[index];
510
+ const expected = group === undefined
511
+ ? { sample_count: null, sample_status: "missing" }
512
+ : {
513
+ sample_count: group.sample_count,
514
+ sample_status: group.sample_status,
515
+ };
516
+ if (
517
+ candidate.sample_count !== expected.sample_count ||
518
+ candidate.sample_status !== expected.sample_status
519
+ ) {
520
+ context.addIssue({
521
+ code: "custom",
522
+ message: "selection candidate must match the aggregate report",
523
+ path: ["selection", "candidates", index],
524
+ });
525
+ }
526
+ });
527
+
528
+ const expectedSelected = expectedGroups.find((group) =>
529
+ group?.sample_status === "sufficient"
530
+ );
531
+ if (
532
+ JSON.stringify(decision.selection.selected_group) !==
533
+ JSON.stringify(expectedSelected)
534
+ ) {
535
+ context.addIssue({
536
+ code: "custom",
537
+ message: "selected_group must match the first sufficient report group",
538
+ path: ["selection", "selected_group"],
539
+ });
540
+ }
541
+ });
542
+
543
+ export type HierarchicalCalibrationScope = z.infer<
544
+ typeof HierarchicalCalibrationScopeSchema
545
+ >;
546
+ export type HierarchicalTaskCalibrationObservation = z.infer<
547
+ typeof HierarchicalTaskCalibrationObservationSchema
548
+ >;
549
+ export type HierarchicalTaskCalibrationGroup = z.infer<
550
+ typeof HierarchicalTaskCalibrationGroupSchema
551
+ >;
552
+ export type HierarchicalTaskCalibrationReport = z.infer<
553
+ typeof HierarchicalTaskCalibrationReportSchema
554
+ >;
555
+ export type HierarchicalTaskCalibrationQuery = z.infer<
556
+ typeof HierarchicalTaskCalibrationQuerySchema
557
+ >;
558
+ export type HierarchicalTaskCalibrationSelection = z.infer<
559
+ typeof HierarchicalTaskCalibrationSelectionSchema
560
+ >;
561
+ export type HierarchicalTaskCalibrationDecision = z.infer<
562
+ typeof HierarchicalTaskCalibrationDecisionSchema
563
+ >;
564
+
565
+ type HierarchicalGroupSeed = {
566
+ scope: HierarchicalCalibrationScope;
567
+ task_type: string;
568
+ task_subtype?: string;
569
+ prompt_pattern?: string;
570
+ source: TaskCalibrationSource;
571
+ observations: HierarchicalTaskCalibrationObservation[];
572
+ };
573
+
574
+ function summarizeHierarchicalGroup(
575
+ seed: HierarchicalGroupSeed,
576
+ minimumSampleCount: number,
577
+ ): HierarchicalTaskCalibrationGroup {
578
+ const sample_count = seed.observations.length;
579
+ const correctCount = seed.observations.reduce(
580
+ (total, observation) => total + Number(observation.correct),
581
+ 0,
582
+ );
583
+ const confidenceTotal = stableSum(
584
+ seed.observations.map((observation) => observation.confidence),
585
+ );
586
+ const brierTotal = stableSum(seed.observations.map((observation) => {
587
+ const outcome = Number(observation.correct);
588
+ return (observation.confidence - outcome) ** 2;
589
+ }));
590
+ const accuracy = stableMetric(correctCount / sample_count);
591
+ const mean_confidence = stableMetric(confidenceTotal / sample_count);
592
+
593
+ return HierarchicalTaskCalibrationGroupSchema.parse({
594
+ scope: seed.scope,
595
+ task_type: seed.task_type,
596
+ ...(seed.task_subtype === undefined
597
+ ? {}
598
+ : { task_subtype: seed.task_subtype }),
599
+ ...(seed.prompt_pattern === undefined
600
+ ? {}
601
+ : { prompt_pattern: seed.prompt_pattern }),
602
+ source: seed.source,
603
+ sample_count,
604
+ accuracy,
605
+ mean_confidence,
606
+ brier_score: stableMetric(brierTotal / sample_count),
607
+ mean_calibration_bias: stableMetric(mean_confidence - accuracy),
608
+ sample_status: sample_count < minimumSampleCount
609
+ ? "insufficient"
610
+ : "sufficient",
611
+ });
612
+ }
613
+
614
+ export function aggregateHierarchicalTaskCalibration(
615
+ observations: readonly unknown[],
616
+ options: TaskCalibrationOptions = {},
617
+ ): HierarchicalTaskCalibrationReport {
618
+ const parsed = HierarchicalTaskCalibrationObservationListSchema.parse(
619
+ observations,
620
+ );
621
+ const { minimum_sample_count } = TaskCalibrationOptionsSchema.parse(options);
622
+ const grouped = new Map<string, HierarchicalGroupSeed>();
623
+
624
+ function add(
625
+ observation: HierarchicalTaskCalibrationObservation,
626
+ scope: HierarchicalCalibrationScope,
627
+ ) {
628
+ const task_subtype = scope === "task_type"
629
+ ? undefined
630
+ : observation.task_subtype;
631
+ const prompt_pattern = scope === "prompt_pattern"
632
+ ? observation.prompt_pattern
633
+ : undefined;
634
+ const key = JSON.stringify([
635
+ scope,
636
+ observation.task_type,
637
+ task_subtype ?? null,
638
+ prompt_pattern ?? null,
639
+ observation.source.provider,
640
+ observation.source.model,
641
+ ]);
642
+ const existing = grouped.get(key);
643
+ if (existing) {
644
+ existing.observations.push(observation);
645
+ return;
646
+ }
647
+ grouped.set(key, {
648
+ scope,
649
+ task_type: observation.task_type,
650
+ ...(task_subtype === undefined ? {} : { task_subtype }),
651
+ ...(prompt_pattern === undefined ? {} : { prompt_pattern }),
652
+ source: observation.source,
653
+ observations: [observation],
654
+ });
655
+ }
656
+
657
+ for (const observation of parsed) {
658
+ add(observation, "task_type");
659
+ if (observation.task_subtype !== undefined) {
660
+ add(observation, "task_subtype");
661
+ }
662
+ if (observation.prompt_pattern !== undefined) {
663
+ add(observation, "prompt_pattern");
664
+ }
665
+ }
666
+
667
+ const scopeOrder: Record<HierarchicalCalibrationScope, number> = {
668
+ task_type: 0,
669
+ task_subtype: 1,
670
+ prompt_pattern: 2,
671
+ };
672
+ const groups = [...grouped.values()].map((seed) =>
673
+ summarizeHierarchicalGroup(seed, minimum_sample_count)
674
+ ).sort((left, right) =>
675
+ compareCodeUnits(left.task_type, right.task_type) ||
676
+ compareCodeUnits(left.source.provider, right.source.provider) ||
677
+ compareCodeUnits(left.source.model, right.source.model) ||
678
+ scopeOrder[left.scope] - scopeOrder[right.scope] ||
679
+ compareCodeUnits(left.task_subtype ?? "", right.task_subtype ?? "") ||
680
+ compareCodeUnits(left.prompt_pattern ?? "", right.prompt_pattern ?? "")
681
+ );
682
+
683
+ return HierarchicalTaskCalibrationReportSchema.parse({
684
+ schema_version: "quorum-router.hierarchical-calibration.v1",
685
+ advisory_only: true,
686
+ minimum_sample_count,
687
+ groups,
688
+ });
689
+ }
690
+
691
+ export function resolveHierarchicalTaskCalibration(
692
+ report: HierarchicalTaskCalibrationReport,
693
+ query: HierarchicalTaskCalibrationQuery,
694
+ ): HierarchicalTaskCalibrationSelection {
695
+ const parsedReport = HierarchicalTaskCalibrationReportSchema.parse(report);
696
+ const parsedQuery = HierarchicalTaskCalibrationQuerySchema.parse(query);
697
+ const requested_scope: HierarchicalCalibrationScope =
698
+ parsedQuery.prompt_pattern !== undefined
699
+ ? "prompt_pattern"
700
+ : parsedQuery.task_subtype !== undefined
701
+ ? "task_subtype"
702
+ : "task_type";
703
+ const scopes: HierarchicalCalibrationScope[] = requested_scope ===
704
+ "prompt_pattern"
705
+ ? ["prompt_pattern", "task_subtype", "task_type"]
706
+ : requested_scope === "task_subtype"
707
+ ? ["task_subtype", "task_type"]
708
+ : ["task_type"];
709
+
710
+ const matchingGroups = scopes.map((scope) =>
711
+ parsedReport.groups.find((group) =>
712
+ group.scope === scope && group.task_type === parsedQuery.task_type &&
713
+ group.source.provider === parsedQuery.source.provider &&
714
+ group.source.model === parsedQuery.source.model &&
715
+ (scope === "task_type" ||
716
+ group.task_subtype === parsedQuery.task_subtype) &&
717
+ (scope !== "prompt_pattern" ||
718
+ group.prompt_pattern === parsedQuery.prompt_pattern)
719
+ )
720
+ );
721
+ const candidates = scopes.map((scope, index) => {
722
+ const group = matchingGroups[index];
723
+ return group === undefined
724
+ ? { scope, sample_count: null, sample_status: "missing" as const }
725
+ : {
726
+ scope,
727
+ sample_count: group.sample_count,
728
+ sample_status: group.sample_status,
729
+ };
730
+ });
731
+ const selected_group = matchingGroups.find((group) =>
732
+ group?.sample_status === "sufficient"
733
+ );
734
+
735
+ return HierarchicalTaskCalibrationSelectionSchema.parse({
736
+ schema_version: "quorum-router.hierarchical-selection.v1",
737
+ advisory_only: true,
738
+ query: parsedQuery,
739
+ requested_scope,
740
+ resolution_status: selected_group === undefined
741
+ ? "no_sufficient_group"
742
+ : "selected",
743
+ selected_scope: selected_group?.scope ?? null,
744
+ candidates,
745
+ ...(selected_group === undefined ? {} : { selected_group }),
746
+ });
747
+ }