@mcptoolshop/research-os 0.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,122 @@
2
2
 
3
3
  All notable changes to `research-os` are documented here.
4
4
 
5
+ ## [0.3.1] — 2026-05-09
6
+
7
+ Tight release. One real, tested, dogfooded improvement: section-scoped
8
+ source-floor waivers + reviewer-side acknowledgement. Earned by Experiment 3
9
+ XRPL pack Session 2 — canonical-protocol sections (XRPL XLS standards,
10
+ single-foundation chain documentation, walled-garden API specs) inverted
11
+ the assumption that publisher diversity is a proxy for truth quality. No
12
+ other v0.3.x candidates shipped — F-01 (init version-stamp), F-02
13
+ (packs-dir docs), F-05 (discover --query example), F-08 (Windows process
14
+ recovery), F-16 (unused SectionSchema fields), F-17 (sections/<id>/gates.yaml
15
+ runtime wiring) are deferred to their own scoped releases.
16
+
17
+ ### Added
18
+
19
+ - **`primary_source_waiver.section_waivers[]`** — section-scoped source-floor
20
+ waivers. Each entry carries `section_id`, `scope` (`min_independent_publishers`
21
+ or `primary_sources_required`), `reason` (non-empty), and
22
+ `compensating_controls[]` (at least one entry). Schema enforcement: empty
23
+ `reason` or empty `compensating_controls[]` fail validation. Pack policy
24
+ `gates.source_floor.primary_source_waiver_allowed: false` blocks both
25
+ pack-level and section-scoped waivers — operators cannot smuggle a waiver
26
+ past pack policy by rerouting it to section scope.
27
+
28
+ Multiple entries can target different sections, or the same section with
29
+ different scopes. Pack-level `primary_source_waiver` semantics unchanged;
30
+ the new `section_waivers[]` is additive and defaults to `[]` for backward
31
+ compatibility. Existing packs unaffected. Full reference:
32
+ [`docs/section-scoped-waivers.md`](docs/section-scoped-waivers.md).
33
+
34
+ - **Reviewer-side acknowledgement** — when a section has a matching
35
+ `min_independent_publishers` waiver in effect, the calibrated reviewer's
36
+ section-wide `source_cluster_monopoly` finding remains visible in the
37
+ findings ledger but does NOT, by itself, route claims to
38
+ `needs_source_repair`. The finding is annotated as
39
+ `(severity, waived)` in the claim-review's reason string so operators
40
+ reading the ledger can see the finding is present but neutralised. Other
41
+ source-quality findings (per-claim `source_quality_problem`,
42
+ `scope_widening`, `overgeneralized_claim`, etc.) continue to drive their
43
+ own routing normally.
44
+
45
+ - **Audit-side disclosure** — `weak-sources.{json,md}` and
46
+ `source-diversity-gaps.{json,md}` rollups annotate waived rows with
47
+ `waived: true` and `waiver_reason: <verbatim>` when a matching section
48
+ waiver is active. Rows are NOT removed (Law 16: waivers do not hide
49
+ evidence). The publisher-monopoly fact is still surfaced in the rollup;
50
+ it's disclosed as deliberately accepted rather than as an open blocker.
51
+
52
+ - **13 new tests** in `test/section-scoped-waivers.test.ts` covering
53
+ schema validation (valid shape, missing reason, empty compensating
54
+ controls, invalid scope enum, bad section_id regex), gate-side
55
+ conversion (section_id match, section_id mismatch, primary_sources_required
56
+ scope, pack-level regression, pack-policy refusal, multiple sections,
57
+ multiple scopes for same section, disclosure in WaiverApplication),
58
+ reviewer-side acknowledgement (waived monopoly → accepted, per-claim
59
+ quality still routes, regression without waiver), and audit-side
60
+ annotation.
61
+
62
+ - **`docs/section-scoped-waivers.md`** — full operator reference: schema,
63
+ behavior contract, valid-cases / invalid-cases enumeration, required
64
+ operator discipline (synthesis-time disclosure beyond the schema), and
65
+ the release thesis. Opens with the canonical phrasing:
66
+ *"Use section-scoped source waivers when publisher diversity is
67
+ structurally incompatible with the section's truth source, not when a
68
+ section merely failed to find enough sources."*
69
+
70
+ - **Handbook page** at `/handbook/section-scoped-waivers` — condensed
71
+ reference matching the docs page.
72
+
73
+ ### Changed
74
+
75
+ - **Pack-level `min_independent_publishers: 0` workaround DEPRECATED**
76
+ in the canonical `research-packs/docs/operator-playbook.md` and the
77
+ research-os handbook mirror. The pack-level pattern remains valid for
78
+ already-frozen packs (e.g., `packages/comfyui-workflow-durability/`)
79
+ whose freeze receipts are unchanged; new packs should prefer the
80
+ section-scoped pattern. Forward notes added to
81
+ [`docs/experiment-1-proof.md`](docs/experiment-1-proof.md) at the
82
+ references to the deprecated workaround — historical content
83
+ preserved, not rewritten.
84
+
85
+ ### Documentation
86
+
87
+ - README status block updated to v0.3.1; version badge updated.
88
+ - `docs/roadmap.md` Experiment 3 progress: section-scoped source waivers
89
+ shipped in v0.3.1; pack #2 of 3 (XRPL) earned both v0.3.0 and v0.3.1
90
+ fixes. Two more external-domain packs required for closure.
91
+ - **Cross-repo:** `research-packs/docs/operator-playbook.md` updated in
92
+ the same release window. Adds the section-scoped waiver pattern as the
93
+ canonical guidance with the same anti-misuse framing as the research-os
94
+ docs page (public guidance is consistent across the surface by design).
95
+ Deprecates the `min_independent_publishers: 0` pack-level workaround.
96
+
97
+ ### Tests
98
+
99
+ - **540 total** (527 at v0.3.0 → 540 at v0.3.1, +13 from
100
+ `test/section-scoped-waivers.test.ts`).
101
+
102
+ ### Migration notes
103
+
104
+ No code-level migration required. Existing packs continue to work
105
+ unchanged — `section_waivers` defaults to `[]`. Frozen packs' freeze
106
+ receipts remain valid (the schema addition is additive).
107
+
108
+ For new canonical-protocol packs: prefer section-scoped waivers over the
109
+ deprecated pack-level `min_independent_publishers: 0` workaround. The
110
+ section-scoped pattern preserves the publisher-diversity floor on every
111
+ section that doesn't waive it explicitly.
112
+
113
+ For operators with packs already using the deprecated pack-level
114
+ workaround: the pattern remains valid; no migration is required. If you
115
+ want to tighten the global default and waive specific sections instead,
116
+ that's a clean per-section migration — set
117
+ `min_independent_publishers` back to its non-zero pack default and add
118
+ section_waivers entries for the sections that need them, with
119
+ `reason` and `compensating_controls[]` documented.
120
+
5
121
  ## [0.3.0] — 2026-05-09
6
122
 
7
123
  Tight release. One real, tested, dogfooded improvement: `--detector` flag on
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
- <a href="https://github.com/mcp-tool-shop-org/research-os/releases/tag/v0.3.0"><img src="https://img.shields.io/badge/version-0.3.0-blue" alt="version 0.3.0"></a>
10
+ <a href="https://github.com/mcp-tool-shop-org/research-os/releases/tag/v0.3.1"><img src="https://img.shields.io/badge/version-0.3.1-blue" alt="version 0.3.1"></a>
11
11
  <a href="https://github.com/mcp-tool-shop-org/research-os/actions/workflows/ci.yml"><img src="https://github.com/mcp-tool-shop-org/research-os/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
12
12
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License"></a>
13
13
  <img src="https://img.shields.io/badge/node-%E2%89%A520-brightgreen" alt="Node ≥20">
@@ -151,9 +151,11 @@ This is the structural alternative to *search → summarize → pretty report*.
151
151
 
152
152
  ## Status
153
153
 
154
- **v0.3.0** — published to npm as `@mcptoolshop/research-os@0.3.0`, 2026-05-09. Ships the `--detector <auto|heuristic|ollama-intern>` flag on `contradict map` (F-09 chain-blocker fix from Experiment 3 Session 1, XRPL pack). 527/527 vitest passing. See [CHANGELOG.md](CHANGELOG.md) and [`docs/contradict-map.md`](docs/contradict-map.md).
154
+ **v0.3.1** — published to npm as `@mcptoolshop/research-os@0.3.1`, 2026-05-09. Ships section-scoped source-floor waivers (`primary_source_waiver.section_waivers[]`) plus reviewer-side acknowledgement so a waived section-wide `source_cluster_monopoly` finding becomes a visible caveat rather than auto-routing all claims to `needs_source_repair`. Earned by Experiment 3 XRPL pack Session 2 — canonical-protocol sections (single-foundation chains, walled-garden API specs, standards-body docs) inverted the assumption that publisher diversity is a proxy for truth quality. 540/540 vitest passing. See [CHANGELOG.md](CHANGELOG.md) and [`docs/section-scoped-waivers.md`](docs/section-scoped-waivers.md).
155
155
 
156
- **`contradict map --detector`**Detector selection is now an explicit operator choice instead of a state-dependent env-var dance. `auto` (default) preserves prior behavior; `heuristic` always works without LLM and completes quickly on narrow-topic documentation sections; `ollama-intern` requires the configured model and exits visibly if unavailable. Mode is announced visibly on every run. The earlier "clear `OLLAMA_INTERN_MODEL` to force heuristic" workaround is superseded by `--detector heuristic` as the canonical operator surface; see the [research-packs operator playbook](https://github.com/mcp-tool-shop-org/research-packs/blob/main/docs/operator-playbook.md) and the [handbook contradict-map page](https://mcp-tool-shop-org.github.io/research-os/handbook/contradict-map/).
156
+ **Section-scoped source waivers**Use them when publisher diversity is structurally incompatible with the section's truth source, not when a section merely failed to find enough sources. Schema-enforced `reason` + non-empty `compensating_controls[]`. Pack policy `primary_source_waiver_allowed: false` blocks both pack-level and section-scoped waivers. The pre-v0.3.1 pack-level `min_independent_publishers: 0` workaround is now deprecated; existing frozen packs remain valid under their existing receipts. See [`docs/section-scoped-waivers.md`](docs/section-scoped-waivers.md) and the [research-packs operator playbook](https://github.com/mcp-tool-shop-org/research-packs/blob/main/docs/operator-playbook.md).
157
+
158
+ **v0.3.0** — published 2026-05-09. Shipped the `--detector <auto|heuristic|ollama-intern>` flag on `contradict map` (F-09 chain-blocker fix from Experiment 3 Session 1, XRPL pack). 527/527 vitest then. Detector selection is now an explicit operator choice instead of a state-dependent env-var dance; mode is announced visibly on every run. See [`docs/contradict-map.md`](docs/contradict-map.md).
157
159
 
158
160
  **v0.2.0** — published 2026-05-09. Shipped `research-os pack publish` (Experiment 2) and the Pattern 2 readiness predicate fix. 515/515 vitest passing then. See [CHANGELOG.md](CHANGELOG.md). Frozen packs export to the canonical `research-packs` archive with a single command; admission contract is enforced by code, not checklist. See [`docs/pack-publish.md`](docs/pack-publish.md).
159
161
 
@@ -165,7 +167,7 @@ This is the structural alternative to *search → summarize → pretty report*.
165
167
 
166
168
  ### What v0.3 is not
167
169
 
168
- - Not battle-tested by external users. Two dogfood arcs have closed — one self-referential, one external-domain — and Experiment 3 (API stability under external pressure) is in progress: pack #1 of 3 (XRPL creator-token durability) earned the v0.3.0 `--detector` flag. Two more external-domain packs required for Experiment 3 closure.
170
+ - Not battle-tested by external users. Two dogfood arcs have closed — one self-referential, one external-domain — and Experiment 3 (API stability under external pressure) is in progress: pack #1 of 3 (XRPL creator-token durability) earned both the v0.3.0 `--detector` flag and the v0.3.1 section-scoped source waivers. Two more external-domain packs required for Experiment 3 closure.
169
171
  - Not a synthesis writer. The `synth workspace` command generates the structured workspace; humans (or Cowork) write the prose against accepted claim IDs.
170
172
  - Not API-stable under semver. v1.0.0 is an earned state, not a calendar date — see [`docs/roadmap.md`](docs/roadmap.md) for the six experiments that close the gap.
171
173
 
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),
@@ -4207,9 +4218,28 @@ function applyWaivers(input, results) {
4207
4218
  const updated = results.map((r) => ({ ...r }));
4208
4219
  const applied = [];
4209
4220
  const validationFailures = [];
4210
- if (waiver.status !== "granted") {
4211
- 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
+ });
4212
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;
4213
4243
  if (!waiver.reason || waiver.reason.trim().length === 0) {
4214
4244
  validationFailures.push({
4215
4245
  family: "waivers",
@@ -4219,7 +4249,7 @@ function applyWaivers(input, results) {
4219
4249
  evidence: [],
4220
4250
  blocks_synthesis: true
4221
4251
  });
4222
- return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4252
+ return;
4223
4253
  }
4224
4254
  if (waiver.compensating_controls.length === 0) {
4225
4255
  validationFailures.push({
@@ -4230,7 +4260,7 @@ function applyWaivers(input, results) {
4230
4260
  evidence: [],
4231
4261
  blocks_synthesis: true
4232
4262
  });
4233
- return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4263
+ return;
4234
4264
  }
4235
4265
  if (!cfg.primary_source_waiver_allowed) {
4236
4266
  validationFailures.push({
@@ -4241,7 +4271,7 @@ function applyWaivers(input, results) {
4241
4271
  evidence: [],
4242
4272
  blocks_synthesis: true
4243
4273
  });
4244
- return { updatedResults: updated, waivers_applied: applied, waiver_validation_failures: validationFailures };
4274
+ return;
4245
4275
  }
4246
4276
  for (let i = 0; i < updated.length; i += 1) {
4247
4277
  const r = updated[i];
@@ -4263,7 +4293,62 @@ function applyWaivers(input, results) {
4263
4293
  });
4264
4294
  }
4265
4295
  }
4266
- 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
+ }
4267
4352
  }
4268
4353
  var init_waivers = __esm({
4269
4354
  "src/gates/checks/waivers.ts"() {
@@ -5520,9 +5605,11 @@ function pickHighestPriority(decisions) {
5520
5605
  return "accepted_for_synthesis";
5521
5606
  }
5522
5607
  function deriveClaimReviews(args) {
5523
- const { claims, findings, reviewer, reviewMethod } = args;
5608
+ const { claims, findings, reviewer, reviewMethod, activeSectionWaivers } = args;
5524
5609
  const reviews = [];
5525
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";
5526
5613
  for (const claim of claims) {
5527
5614
  const claimFindings = findings.filter((f) => f.claim_ids.includes(claim.claim_id));
5528
5615
  if (claimFindings.length === 0) {
@@ -5539,6 +5626,7 @@ function deriveClaimReviews(args) {
5539
5626
  }
5540
5627
  let decisions = [];
5541
5628
  for (const f of claimFindings) {
5629
+ if (isWaivedFinding(f)) continue;
5542
5630
  if (f.severity === "block") {
5543
5631
  decisions.push(BLOCK_TO_DECISION[f.category] ?? "rejected");
5544
5632
  } else if (f.severity === "warn") {
@@ -5555,7 +5643,9 @@ function deriveClaimReviews(args) {
5555
5643
  );
5556
5644
  }
5557
5645
  const decision = pickHighestPriority(decisions);
5558
- 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
+ );
5559
5649
  const reason = reasonParts.length > 0 ? `Findings: ${reasonParts.join("; ")}.` : "Only info-level findings; accepted.";
5560
5650
  reviews.push({
5561
5651
  claim_id: claim.claim_id,
@@ -5936,7 +6026,8 @@ async function review(options) {
5936
6026
  candidateClaims,
5937
6027
  drafts: acceptedDrafts,
5938
6028
  llmFindingsRejected,
5939
- profile: options.profile ?? DEFAULT_PROFILE
6029
+ profile: options.profile ?? DEFAULT_PROFILE,
6030
+ research
5940
6031
  });
5941
6032
  }
5942
6033
  async function runMultiPassReview(args) {
@@ -5998,7 +6089,8 @@ async function runMultiPassReview(args) {
5998
6089
  candidateClaims: args.candidateClaims,
5999
6090
  drafts: merged,
6000
6091
  llmFindingsRejected,
6001
- profile: args.options.profile ?? DEFAULT_PROFILE
6092
+ profile: args.options.profile ?? DEFAULT_PROFILE,
6093
+ research: args.research
6002
6094
  });
6003
6095
  }
6004
6096
  async function reviewWithSpecificReviewer(args) {
@@ -6026,7 +6118,8 @@ async function reviewWithSpecificReviewer(args) {
6026
6118
  candidateClaims: args.candidateClaims,
6027
6119
  drafts: result.drafts,
6028
6120
  llmFindingsRejected: 0,
6029
- profile: args.options.profile ?? DEFAULT_PROFILE
6121
+ profile: args.options.profile ?? DEFAULT_PROFILE,
6122
+ research: args.research
6030
6123
  });
6031
6124
  }
6032
6125
  async function finalizeReview(args) {
@@ -6049,11 +6142,15 @@ async function finalizeReview(args) {
6049
6142
  seen.add(f.finding_id);
6050
6143
  dedupedFindings.push(f);
6051
6144
  }
6145
+ const activeSectionWaivers = args.research.primary_source_waiver.section_waivers.filter(
6146
+ (w) => w.section_id === args.sectionId
6147
+ );
6052
6148
  const claimReviews = deriveClaimReviews({
6053
6149
  claims: args.candidateClaims,
6054
6150
  findings: dedupedFindings,
6055
6151
  reviewer: args.reviewer,
6056
- reviewMethod: args.reviewMethod
6152
+ reviewMethod: args.reviewMethod,
6153
+ activeSectionWaivers
6057
6154
  });
6058
6155
  const decisionCounts = {
6059
6156
  accepted_for_synthesis: 0,
@@ -8780,6 +8877,19 @@ function buildStaleSources(input) {
8780
8877
  }
8781
8878
  return out;
8782
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
+ }
8783
8893
  function buildWeakSources(input) {
8784
8894
  const out = [];
8785
8895
  const cfg = input.research.gates.source_floor;
@@ -8789,33 +8899,50 @@ function buildWeakSources(input) {
8789
8899
  const publishers = new Set(
8790
8900
  sectionSources.map((c) => c.publisher).filter((p) => typeof p === "string")
8791
8901
  );
8902
+ const monopolyWaiver = findSectionWaiver(input.research, sid, "min_independent_publishers");
8903
+ const primaryWaiver = findSectionWaiver(input.research, sid, "primary_sources_required");
8792
8904
  if (publishers.size === 1 && sectionSources.length >= 2) {
8793
- out.push({
8794
- reason: "source_cluster_monopoly",
8795
- section_id: sid,
8796
- details: `Every source in this section traces to a single publisher (${[...publishers][0]}).`,
8797
- evidence_ids: sectionSources.map((s) => s.source_id),
8798
- artifact_path: `sections/${sid}/sources.jsonl`
8799
- });
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
+ );
8800
8917
  }
8801
8918
  if (publishers.size < cfg.min_independent_publishers) {
8802
- out.push({
8803
- reason: "low_independent_publishers",
8804
- section_id: sid,
8805
- details: `${publishers.size} independent publisher(s) \u2014 pack policy requires at least ${cfg.min_independent_publishers}.`,
8806
- evidence_ids: [...publishers],
8807
- artifact_path: `sections/${sid}/sources.jsonl`
8808
- });
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
+ );
8809
8931
  }
8810
8932
  const primary = sectionSources.filter((c) => c.source_type === "primary").length;
8811
8933
  if (primary < cfg.primary_sources_required) {
8812
- out.push({
8813
- reason: "missing_primary_source",
8814
- section_id: sid,
8815
- details: `${primary} primary source(s) \u2014 pack policy requires at least ${cfg.primary_sources_required}.`,
8816
- evidence_ids: sectionSources.filter((c) => c.source_type === "primary").map((c) => c.source_id),
8817
- artifact_path: `sections/${sid}/sources.jsonl`
8818
- });
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
+ );
8819
8946
  }
8820
8947
  const types = /* @__PURE__ */ new Map();
8821
8948
  for (const c of sectionSources) types.set(c.source_type, (types.get(c.source_type) ?? 0) + 1);
@@ -8930,20 +9057,31 @@ function buildSourceDiversityGaps(input) {
8930
9057
  });
8931
9058
  continue;
8932
9059
  }
9060
+ const monopolyWaiver = findSectionWaiver(input.research, sid, "min_independent_publishers");
8933
9061
  if (publishers.size === 1 && sectionSources.length >= 2) {
8934
- out.push({
8935
- reason: "section_publisher_monopoly",
8936
- section_id: sid,
8937
- details: `Section sources monopolized by ${[...publishers][0]}.`,
8938
- evidence_ids: sectionSources.map((s) => s.source_id)
8939
- });
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
+ );
8940
9073
  } else if (publishers.size < cfg.min_independent_publishers) {
8941
- out.push({
8942
- reason: "low_section_publisher_count",
8943
- section_id: sid,
8944
- details: `${publishers.size} publisher(s); pack policy requires ${cfg.min_independent_publishers}.`,
8945
- evidence_ids: [...publishers]
8946
- });
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
+ );
8947
9085
  }
8948
9086
  }
8949
9087
  const pubSectionCount = /* @__PURE__ */ new Map();
@@ -11734,7 +11872,7 @@ var init_src = __esm({
11734
11872
  init_triage();
11735
11873
  init_discover();
11736
11874
  init_errors();
11737
- RESEARCH_OS_VERSION = "0.3.0";
11875
+ RESEARCH_OS_VERSION = "0.3.1";
11738
11876
  }
11739
11877
  });
11740
11878