@mcptoolshop/research-os 0.2.0 → 0.3.1

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/dist/cli.js CHANGED
@@ -106,11 +106,12 @@ __export(schema_exports, {
106
106
  ReviewProfilePresetSchema: () => ReviewProfilePresetSchema,
107
107
  SectionBudgetGateSchema: () => SectionBudgetGateSchema,
108
108
  SectionSchema: () => SectionSchema,
109
+ SectionScopedWaiverSchema: () => SectionScopedWaiverSchema,
109
110
  SectionStatusSchema: () => SectionStatusSchema,
110
111
  SourceFloorGateSchema: () => SourceFloorGateSchema
111
112
  });
112
113
  import { z } from "zod";
113
- var SectionStatusSchema, SectionSchema, SourceFloorGateSchema, ClaimIntegrityGateSchema, FreshnessGateSchema, ContradictionGateSchema, SectionBudgetGateSchema, GateConfigSchema, PrimarySourceWaiverSchema, FreshnessRequirementsSchema, ReviewProfilePresetSchema, DEFAULT_REVIEW_PROFILES, ResearchYamlSchema;
114
+ var SectionStatusSchema, SectionSchema, SourceFloorGateSchema, ClaimIntegrityGateSchema, FreshnessGateSchema, ContradictionGateSchema, SectionBudgetGateSchema, GateConfigSchema, SectionScopedWaiverSchema, PrimarySourceWaiverSchema, FreshnessRequirementsSchema, ReviewProfilePresetSchema, DEFAULT_REVIEW_PROFILES, ResearchYamlSchema;
114
115
  var init_schema = __esm({
115
116
  "src/intake/schema.ts"() {
116
117
  "use strict";
@@ -160,10 +161,20 @@ var init_schema = __esm({
160
161
  contradiction: ContradictionGateSchema.default({}),
161
162
  section_budget: SectionBudgetGateSchema.default({})
162
163
  });
164
+ SectionScopedWaiverSchema = z.object({
165
+ section_id: z.string().regex(/^[0-9]{2}-[a-z0-9-]+$/, 'Section id must look like "01-landscape"'),
166
+ scope: z.enum(["min_independent_publishers", "primary_sources_required"]),
167
+ reason: z.string().min(1),
168
+ compensating_controls: z.array(z.string()).min(1)
169
+ });
163
170
  PrimarySourceWaiverSchema = z.object({
164
171
  status: z.enum(["none", "requested", "granted"]).default("none"),
165
172
  reason: z.string().optional(),
166
- compensating_controls: z.array(z.string()).default([])
173
+ compensating_controls: z.array(z.string()).default([]),
174
+ // Section-scoped waivers; each entry is its own waiver record. Independent
175
+ // of the pack-level status/reason/compensating_controls fields above.
176
+ // Defaults to [] for backward compatibility — existing packs unaffected.
177
+ section_waivers: z.array(SectionScopedWaiverSchema).default([])
167
178
  });
168
179
  FreshnessRequirementsSchema = z.object({
169
180
  required: z.boolean().default(true),
@@ -2774,9 +2785,6 @@ Hard rules:
2774
2785
  });
2775
2786
 
2776
2787
  // src/contradictions/detectors/index.ts
2777
- function defaultContradictionDetectors() {
2778
- return [new OllamaInternContradictionDetector(), new HeuristicContradictionDetector()];
2779
- }
2780
2788
  async function pickContradictionDetector(detectors) {
2781
2789
  for (const d of detectors) {
2782
2790
  if (await d.available()) return d;
@@ -3250,6 +3258,48 @@ function buildContradiction(args) {
3250
3258
  created_at: (/* @__PURE__ */ new Date()).toISOString()
3251
3259
  });
3252
3260
  }
3261
+ async function resolveDetector(options) {
3262
+ const mode = options.detectorMode ?? "auto";
3263
+ if (!VALID_DETECTOR_MODES.includes(mode)) {
3264
+ throw new Error(
3265
+ `contradict map: invalid --detector value "${mode}"; valid values are: auto, heuristic, ollama-intern`
3266
+ );
3267
+ }
3268
+ if (mode === "heuristic") {
3269
+ return {
3270
+ detector: new HeuristicContradictionDetector(),
3271
+ announcement: "contradict map: using heuristic detector"
3272
+ };
3273
+ }
3274
+ if (mode === "ollama-intern") {
3275
+ const d = new OllamaInternContradictionDetector(options.ollamaConfig ?? {});
3276
+ if (!await d.available()) {
3277
+ throw new Error(
3278
+ `contradict map: ollama-intern detector requested but model ${d.model} is unavailable; aborting (use --detector heuristic to bypass)`
3279
+ );
3280
+ }
3281
+ return {
3282
+ detector: d,
3283
+ announcement: `contradict map: using ollama-intern detector with model ${d.model}`
3284
+ };
3285
+ }
3286
+ const detectors = options.detectors ?? [
3287
+ new OllamaInternContradictionDetector(options.ollamaConfig ?? {}),
3288
+ new HeuristicContradictionDetector()
3289
+ ];
3290
+ const detector = await pickContradictionDetector(detectors);
3291
+ if (detector.name === "ollama-intern") {
3292
+ const modelName = detector instanceof OllamaInternContradictionDetector ? detector.model : process.env.OLLAMA_INTERN_MODEL ?? "hermes3:8b";
3293
+ return {
3294
+ detector,
3295
+ announcement: `contradict map: using ollama-intern detector with model ${modelName}`
3296
+ };
3297
+ }
3298
+ return {
3299
+ detector,
3300
+ announcement: "contradict map: ollama-intern unavailable; using heuristic detector"
3301
+ };
3302
+ }
3253
3303
  async function map(options) {
3254
3304
  const packPath = options.packPath ? resolve7(options.packPath) : process.cwd();
3255
3305
  if (!existsSync9(join10(packPath, "research.yaml"))) throw new PackNotFoundError(packPath);
@@ -3261,8 +3311,7 @@ async function map(options) {
3261
3311
  const allowed = await readTriagedClaimIds2(packPath, options.sectionId);
3262
3312
  candidateClaims = candidateClaims.filter((c) => allowed.has(c.claim_id));
3263
3313
  }
3264
- const adapters = options.detectors ?? defaultContradictionDetectors();
3265
- const detector = await pickContradictionDetector(adapters);
3314
+ const { detector, announcement } = await resolveDetector(options);
3266
3315
  const summary = {
3267
3316
  sectionId: options.sectionId,
3268
3317
  detector: detector.name,
@@ -3272,7 +3321,8 @@ async function map(options) {
3272
3321
  contradictionsAdded: 0,
3273
3322
  contradictionsDeduped: 0,
3274
3323
  contradictionIds: [],
3275
- detectorError: null
3324
+ detectorError: null,
3325
+ detectorAnnouncement: announcement
3276
3326
  };
3277
3327
  const ledgerPath = join10(sectionDir, "contradictions.jsonl");
3278
3328
  const mdPath = join10(sectionDir, "contradictions.md");
@@ -3336,7 +3386,7 @@ async function map(options) {
3336
3386
  await writeFile9(mdPath, md, "utf8");
3337
3387
  return summary;
3338
3388
  }
3339
- var DETECTOR_ID_PART;
3389
+ var DETECTOR_ID_PART, VALID_DETECTOR_MODES;
3340
3390
  var init_map = __esm({
3341
3391
  "src/contradictions/map.ts"() {
3342
3392
  "use strict";
@@ -3344,11 +3394,14 @@ var init_map = __esm({
3344
3394
  init_schema5();
3345
3395
  init_schema7();
3346
3396
  init_detectors();
3397
+ init_heuristic3();
3398
+ init_ollama_intern3();
3347
3399
  init_markdown();
3348
3400
  DETECTOR_ID_PART = {
3349
3401
  heuristic: "heuristic",
3350
3402
  "ollama-intern": "ollama_intern"
3351
3403
  };
3404
+ VALID_DETECTOR_MODES = ["auto", "heuristic", "ollama-intern"];
3352
3405
  }
3353
3406
  });
3354
3407
 
@@ -4165,9 +4218,28 @@ function applyWaivers(input, results) {
4165
4218
  const updated = results.map((r) => ({ ...r }));
4166
4219
  const applied = [];
4167
4220
  const validationFailures = [];
4168
- if (waiver.status !== "granted") {
4169
- return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4221
+ applyPackLevelWaiver({
4222
+ waiver,
4223
+ cfg,
4224
+ updated,
4225
+ applied,
4226
+ validationFailures
4227
+ });
4228
+ for (const sw of waiver.section_waivers ?? []) {
4229
+ if (sw.section_id !== input.section.id) continue;
4230
+ applySectionScopedWaiver({
4231
+ waiver: sw,
4232
+ cfg,
4233
+ updated,
4234
+ applied,
4235
+ validationFailures
4236
+ });
4170
4237
  }
4238
+ return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4239
+ }
4240
+ function applyPackLevelWaiver(args) {
4241
+ const { waiver, cfg, updated, applied, validationFailures } = args;
4242
+ if (waiver.status !== "granted") return;
4171
4243
  if (!waiver.reason || waiver.reason.trim().length === 0) {
4172
4244
  validationFailures.push({
4173
4245
  family: "waivers",
@@ -4177,7 +4249,7 @@ function applyWaivers(input, results) {
4177
4249
  evidence: [],
4178
4250
  blocks_synthesis: true
4179
4251
  });
4180
- return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4252
+ return;
4181
4253
  }
4182
4254
  if (waiver.compensating_controls.length === 0) {
4183
4255
  validationFailures.push({
@@ -4188,7 +4260,7 @@ function applyWaivers(input, results) {
4188
4260
  evidence: [],
4189
4261
  blocks_synthesis: true
4190
4262
  });
4191
- return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4263
+ return;
4192
4264
  }
4193
4265
  if (!cfg.primary_source_waiver_allowed) {
4194
4266
  validationFailures.push({
@@ -4199,7 +4271,7 @@ function applyWaivers(input, results) {
4199
4271
  evidence: [],
4200
4272
  blocks_synthesis: true
4201
4273
  });
4202
- return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4274
+ return;
4203
4275
  }
4204
4276
  for (let i = 0; i < updated.length; i += 1) {
4205
4277
  const r = updated[i];
@@ -4221,7 +4293,62 @@ function applyWaivers(input, results) {
4221
4293
  });
4222
4294
  }
4223
4295
  }
4224
- return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4296
+ }
4297
+ function applySectionScopedWaiver(args) {
4298
+ const { waiver, cfg, updated, applied, validationFailures } = args;
4299
+ if (!waiver.reason || waiver.reason.trim().length === 0) {
4300
+ validationFailures.push({
4301
+ family: "waivers",
4302
+ check: "section_scoped_waiver_reason_required",
4303
+ status: "fail",
4304
+ detail: `Section-scoped waiver for ${waiver.section_id}/${waiver.scope} is missing a reason. Waiver is invalid; original failure stands.`,
4305
+ evidence: [waiver.section_id],
4306
+ blocks_synthesis: true
4307
+ });
4308
+ return;
4309
+ }
4310
+ if (waiver.compensating_controls.length === 0) {
4311
+ validationFailures.push({
4312
+ family: "waivers",
4313
+ check: "section_scoped_waiver_compensating_controls_required",
4314
+ status: "fail",
4315
+ detail: `Section-scoped waiver for ${waiver.section_id}/${waiver.scope} has empty compensating_controls. Waiver is invalid; original failure stands.`,
4316
+ evidence: [waiver.section_id],
4317
+ blocks_synthesis: true
4318
+ });
4319
+ return;
4320
+ }
4321
+ if (!cfg.primary_source_waiver_allowed) {
4322
+ validationFailures.push({
4323
+ family: "waivers",
4324
+ check: "section_scoped_waiver_allowed_by_pack",
4325
+ status: "fail",
4326
+ detail: `Pack policy gates.source_floor.primary_source_waiver_allowed=false; section-scoped waiver for ${waiver.section_id}/${waiver.scope} cannot be applied.`,
4327
+ evidence: [waiver.section_id],
4328
+ blocks_synthesis: true
4329
+ });
4330
+ return;
4331
+ }
4332
+ for (let i = 0; i < updated.length; i += 1) {
4333
+ const r = updated[i];
4334
+ if (r.family === "source_floor" && r.check === waiver.scope && r.status === "fail") {
4335
+ const original = r.status;
4336
+ updated[i] = {
4337
+ ...r,
4338
+ status: "pass_with_waiver",
4339
+ detail: `${r.detail} Section-scoped waiver granted for ${waiver.section_id} with ${waiver.compensating_controls.length} compensating control(s); converted from fail to pass_with_waiver.`,
4340
+ blocks_synthesis: false
4341
+ };
4342
+ applied.push({
4343
+ family: "source_floor",
4344
+ check: waiver.scope,
4345
+ reason: waiver.reason,
4346
+ compensating_controls: waiver.compensating_controls,
4347
+ original_status: original,
4348
+ new_status: "pass_with_waiver"
4349
+ });
4350
+ }
4351
+ }
4225
4352
  }
4226
4353
  var init_waivers = __esm({
4227
4354
  "src/gates/checks/waivers.ts"() {
@@ -5478,9 +5605,11 @@ function pickHighestPriority(decisions) {
5478
5605
  return "accepted_for_synthesis";
5479
5606
  }
5480
5607
  function deriveClaimReviews(args) {
5481
- const { claims, findings, reviewer, reviewMethod } = args;
5608
+ const { claims, findings, reviewer, reviewMethod, activeSectionWaivers } = args;
5482
5609
  const reviews = [];
5483
5610
  const now = (/* @__PURE__ */ new Date()).toISOString();
5611
+ const monopolyWaived = Array.isArray(activeSectionWaivers) && activeSectionWaivers.some((w) => w.scope === "min_independent_publishers");
5612
+ const isWaivedFinding = (f) => monopolyWaived && f.category === "source_cluster_monopoly";
5484
5613
  for (const claim of claims) {
5485
5614
  const claimFindings = findings.filter((f) => f.claim_ids.includes(claim.claim_id));
5486
5615
  if (claimFindings.length === 0) {
@@ -5497,6 +5626,7 @@ function deriveClaimReviews(args) {
5497
5626
  }
5498
5627
  let decisions = [];
5499
5628
  for (const f of claimFindings) {
5629
+ if (isWaivedFinding(f)) continue;
5500
5630
  if (f.severity === "block") {
5501
5631
  decisions.push(BLOCK_TO_DECISION[f.category] ?? "rejected");
5502
5632
  } else if (f.severity === "warn") {
@@ -5513,7 +5643,9 @@ function deriveClaimReviews(args) {
5513
5643
  );
5514
5644
  }
5515
5645
  const decision = pickHighestPriority(decisions);
5516
- const reasonParts = claimFindings.filter((f) => f.severity !== "info").map((f) => `${f.category} (${f.severity})`);
5646
+ const reasonParts = claimFindings.filter((f) => f.severity !== "info").map(
5647
+ (f) => isWaivedFinding(f) ? `${f.category} (${f.severity}, waived)` : `${f.category} (${f.severity})`
5648
+ );
5517
5649
  const reason = reasonParts.length > 0 ? `Findings: ${reasonParts.join("; ")}.` : "Only info-level findings; accepted.";
5518
5650
  reviews.push({
5519
5651
  claim_id: claim.claim_id,
@@ -5894,7 +6026,8 @@ async function review(options) {
5894
6026
  candidateClaims,
5895
6027
  drafts: acceptedDrafts,
5896
6028
  llmFindingsRejected,
5897
- profile: options.profile ?? DEFAULT_PROFILE
6029
+ profile: options.profile ?? DEFAULT_PROFILE,
6030
+ research
5898
6031
  });
5899
6032
  }
5900
6033
  async function runMultiPassReview(args) {
@@ -5956,7 +6089,8 @@ async function runMultiPassReview(args) {
5956
6089
  candidateClaims: args.candidateClaims,
5957
6090
  drafts: merged,
5958
6091
  llmFindingsRejected,
5959
- profile: args.options.profile ?? DEFAULT_PROFILE
6092
+ profile: args.options.profile ?? DEFAULT_PROFILE,
6093
+ research: args.research
5960
6094
  });
5961
6095
  }
5962
6096
  async function reviewWithSpecificReviewer(args) {
@@ -5984,7 +6118,8 @@ async function reviewWithSpecificReviewer(args) {
5984
6118
  candidateClaims: args.candidateClaims,
5985
6119
  drafts: result.drafts,
5986
6120
  llmFindingsRejected: 0,
5987
- profile: args.options.profile ?? DEFAULT_PROFILE
6121
+ profile: args.options.profile ?? DEFAULT_PROFILE,
6122
+ research: args.research
5988
6123
  });
5989
6124
  }
5990
6125
  async function finalizeReview(args) {
@@ -6007,11 +6142,15 @@ async function finalizeReview(args) {
6007
6142
  seen.add(f.finding_id);
6008
6143
  dedupedFindings.push(f);
6009
6144
  }
6145
+ const activeSectionWaivers = args.research.primary_source_waiver.section_waivers.filter(
6146
+ (w) => w.section_id === args.sectionId
6147
+ );
6010
6148
  const claimReviews = deriveClaimReviews({
6011
6149
  claims: args.candidateClaims,
6012
6150
  findings: dedupedFindings,
6013
6151
  reviewer: args.reviewer,
6014
- reviewMethod: args.reviewMethod
6152
+ reviewMethod: args.reviewMethod,
6153
+ activeSectionWaivers
6015
6154
  });
6016
6155
  const decisionCounts = {
6017
6156
  accepted_for_synthesis: 0,
@@ -8738,6 +8877,19 @@ function buildStaleSources(input) {
8738
8877
  }
8739
8878
  return out;
8740
8879
  }
8880
+ function findSectionWaiver(research, sectionId, scope) {
8881
+ return (research.primary_source_waiver.section_waivers ?? []).find(
8882
+ (w) => w.section_id === sectionId && w.scope === scope
8883
+ );
8884
+ }
8885
+ function annotateWeakSource(row, waiver) {
8886
+ if (!waiver) return row;
8887
+ return { ...row, waived: true, waiver_reason: waiver.reason };
8888
+ }
8889
+ function annotateDiversityGap(row, waiver) {
8890
+ if (!waiver) return row;
8891
+ return { ...row, waived: true, waiver_reason: waiver.reason };
8892
+ }
8741
8893
  function buildWeakSources(input) {
8742
8894
  const out = [];
8743
8895
  const cfg = input.research.gates.source_floor;
@@ -8747,33 +8899,50 @@ function buildWeakSources(input) {
8747
8899
  const publishers = new Set(
8748
8900
  sectionSources.map((c) => c.publisher).filter((p) => typeof p === "string")
8749
8901
  );
8902
+ const monopolyWaiver = findSectionWaiver(input.research, sid, "min_independent_publishers");
8903
+ const primaryWaiver = findSectionWaiver(input.research, sid, "primary_sources_required");
8750
8904
  if (publishers.size === 1 && sectionSources.length >= 2) {
8751
- out.push({
8752
- reason: "source_cluster_monopoly",
8753
- section_id: sid,
8754
- details: `Every source in this section traces to a single publisher (${[...publishers][0]}).`,
8755
- evidence_ids: sectionSources.map((s) => s.source_id),
8756
- artifact_path: `sections/${sid}/sources.jsonl`
8757
- });
8905
+ out.push(
8906
+ annotateWeakSource(
8907
+ {
8908
+ reason: "source_cluster_monopoly",
8909
+ section_id: sid,
8910
+ details: `Every source in this section traces to a single publisher (${[...publishers][0]}).`,
8911
+ evidence_ids: sectionSources.map((s) => s.source_id),
8912
+ artifact_path: `sections/${sid}/sources.jsonl`
8913
+ },
8914
+ monopolyWaiver
8915
+ )
8916
+ );
8758
8917
  }
8759
8918
  if (publishers.size < cfg.min_independent_publishers) {
8760
- out.push({
8761
- reason: "low_independent_publishers",
8762
- section_id: sid,
8763
- details: `${publishers.size} independent publisher(s) \u2014 pack policy requires at least ${cfg.min_independent_publishers}.`,
8764
- evidence_ids: [...publishers],
8765
- artifact_path: `sections/${sid}/sources.jsonl`
8766
- });
8919
+ out.push(
8920
+ annotateWeakSource(
8921
+ {
8922
+ reason: "low_independent_publishers",
8923
+ section_id: sid,
8924
+ details: `${publishers.size} independent publisher(s) \u2014 pack policy requires at least ${cfg.min_independent_publishers}.`,
8925
+ evidence_ids: [...publishers],
8926
+ artifact_path: `sections/${sid}/sources.jsonl`
8927
+ },
8928
+ monopolyWaiver
8929
+ )
8930
+ );
8767
8931
  }
8768
8932
  const primary = sectionSources.filter((c) => c.source_type === "primary").length;
8769
8933
  if (primary < cfg.primary_sources_required) {
8770
- out.push({
8771
- reason: "missing_primary_source",
8772
- section_id: sid,
8773
- details: `${primary} primary source(s) \u2014 pack policy requires at least ${cfg.primary_sources_required}.`,
8774
- evidence_ids: sectionSources.filter((c) => c.source_type === "primary").map((c) => c.source_id),
8775
- artifact_path: `sections/${sid}/sources.jsonl`
8776
- });
8934
+ out.push(
8935
+ annotateWeakSource(
8936
+ {
8937
+ reason: "missing_primary_source",
8938
+ section_id: sid,
8939
+ details: `${primary} primary source(s) \u2014 pack policy requires at least ${cfg.primary_sources_required}.`,
8940
+ evidence_ids: sectionSources.filter((c) => c.source_type === "primary").map((c) => c.source_id),
8941
+ artifact_path: `sections/${sid}/sources.jsonl`
8942
+ },
8943
+ primaryWaiver
8944
+ )
8945
+ );
8777
8946
  }
8778
8947
  const types = /* @__PURE__ */ new Map();
8779
8948
  for (const c of sectionSources) types.set(c.source_type, (types.get(c.source_type) ?? 0) + 1);
@@ -8888,20 +9057,31 @@ function buildSourceDiversityGaps(input) {
8888
9057
  });
8889
9058
  continue;
8890
9059
  }
9060
+ const monopolyWaiver = findSectionWaiver(input.research, sid, "min_independent_publishers");
8891
9061
  if (publishers.size === 1 && sectionSources.length >= 2) {
8892
- out.push({
8893
- reason: "section_publisher_monopoly",
8894
- section_id: sid,
8895
- details: `Section sources monopolized by ${[...publishers][0]}.`,
8896
- evidence_ids: sectionSources.map((s) => s.source_id)
8897
- });
9062
+ out.push(
9063
+ annotateDiversityGap(
9064
+ {
9065
+ reason: "section_publisher_monopoly",
9066
+ section_id: sid,
9067
+ details: `Section sources monopolized by ${[...publishers][0]}.`,
9068
+ evidence_ids: sectionSources.map((s) => s.source_id)
9069
+ },
9070
+ monopolyWaiver
9071
+ )
9072
+ );
8898
9073
  } else if (publishers.size < cfg.min_independent_publishers) {
8899
- out.push({
8900
- reason: "low_section_publisher_count",
8901
- section_id: sid,
8902
- details: `${publishers.size} publisher(s); pack policy requires ${cfg.min_independent_publishers}.`,
8903
- evidence_ids: [...publishers]
8904
- });
9074
+ out.push(
9075
+ annotateDiversityGap(
9076
+ {
9077
+ reason: "low_section_publisher_count",
9078
+ section_id: sid,
9079
+ details: `${publishers.size} publisher(s); pack policy requires ${cfg.min_independent_publishers}.`,
9080
+ evidence_ids: [...publishers]
9081
+ },
9082
+ monopolyWaiver
9083
+ )
9084
+ );
8905
9085
  }
8906
9086
  }
8907
9087
  const pubSectionCount = /* @__PURE__ */ new Map();
@@ -11692,7 +11872,7 @@ var init_src = __esm({
11692
11872
  init_triage();
11693
11873
  init_discover();
11694
11874
  init_errors();
11695
- RESEARCH_OS_VERSION = "0.2.0";
11875
+ RESEARCH_OS_VERSION = "0.3.1";
11696
11876
  }
11697
11877
  });
11698
11878
 
@@ -12123,7 +12303,7 @@ init_synth();
12123
12303
  init_audit();
12124
12304
  init_freeze();
12125
12305
  init_invalidate();
12126
- import { Command } from "commander";
12306
+ import { Command, Option } from "commander";
12127
12307
 
12128
12308
  // src/pack/publish/index.ts
12129
12309
  import { existsSync as existsSync30, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
@@ -13054,13 +13234,21 @@ contradictCmd.command("map").description("Detect contradiction candidates among
13054
13234
  "--triaged-only",
13055
13235
  "Only consider claims that triage selected_for_review; reduces N\xB2 pair classification on dense sections",
13056
13236
  false
13237
+ ).addOption(
13238
+ new Option(
13239
+ "--detector <mode>",
13240
+ "Detector to use: auto (default, env-var-driven), heuristic (always fast, no LLM), ollama-intern (require LLM, fail visibly if unavailable)"
13241
+ ).choices(["auto", "heuristic", "ollama-intern"]).default("auto")
13057
13242
  ).action(async (section, opts) => {
13058
13243
  try {
13059
13244
  const result = await map({
13060
13245
  sectionId: section,
13061
13246
  packPath: opts.pack,
13062
- triagedOnly: opts.triagedOnly
13247
+ triagedOnly: opts.triagedOnly,
13248
+ detectorMode: opts.detector
13063
13249
  });
13250
+ process.stdout.write(`${result.detectorAnnouncement}
13251
+ `);
13064
13252
  process.stdout.write(`contradiction map complete
13065
13253
  `);
13066
13254
  process.stdout.write(` section: ${result.sectionId}