@mcptoolshop/research-os 0.3.0 → 0.3.2

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),
@@ -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.2";
11738
11876
  }
11739
11877
  });
11740
11878
 
@@ -12173,12 +12311,55 @@ import { join as join31, basename, resolve as resolve24 } from "path";
12173
12311
 
12174
12312
  // src/pack/publish/manifest.ts
12175
12313
  init_schema();
12314
+ init_schema9();
12176
12315
  import { createHash as createHash11 } from "crypto";
12177
12316
  import { readFileSync, existsSync as existsSync28 } from "fs";
12178
12317
  import { join as join28 } from "path";
12179
12318
  import { parse as parseYaml } from "yaml";
12180
12319
  import { z as z23 } from "zod";
12181
12320
 
12321
+ // src/closure-ledger/effective-accepted.ts
12322
+ function getEffectiveDecisionMap(reviews) {
12323
+ const map2 = /* @__PURE__ */ new Map();
12324
+ for (const r of reviews) {
12325
+ const existing = map2.get(r.claim_id);
12326
+ if (!existing || r.created_at > existing.created_at) {
12327
+ map2.set(r.claim_id, r);
12328
+ }
12329
+ }
12330
+ return map2;
12331
+ }
12332
+ function getEffectiveAcceptedClaimIds(reviews) {
12333
+ const decisionMap = getEffectiveDecisionMap(reviews);
12334
+ const accepted = /* @__PURE__ */ new Set();
12335
+ for (const [claim_id, r] of decisionMap) {
12336
+ if (r.decision === "accepted_for_synthesis") accepted.add(claim_id);
12337
+ }
12338
+ return accepted;
12339
+ }
12340
+ function findIncompatibleDecisions(reviews) {
12341
+ const groups = /* @__PURE__ */ new Map();
12342
+ for (const r of reviews) {
12343
+ const key = `${r.claim_id}|${r.created_at}`;
12344
+ let set = groups.get(key);
12345
+ if (!set) {
12346
+ set = /* @__PURE__ */ new Set();
12347
+ groups.set(key, set);
12348
+ }
12349
+ set.add(r.decision);
12350
+ }
12351
+ const conflicts = [];
12352
+ for (const [key, decisions] of groups) {
12353
+ if (decisions.size > 1) {
12354
+ const sep3 = key.lastIndexOf("|");
12355
+ const claim_id = key.slice(0, sep3);
12356
+ const created_at = key.slice(sep3 + 1);
12357
+ conflicts.push({ claim_id, created_at, decisions: [...decisions] });
12358
+ }
12359
+ }
12360
+ return conflicts;
12361
+ }
12362
+
12182
12363
  // src/pack/publish/schema.ts
12183
12364
  import { z as z22 } from "zod";
12184
12365
  var SectionSummarySchema = z22.object({
@@ -12210,6 +12391,7 @@ var GateResultMinimalSchema = z23.object({
12210
12391
  verdict: z23.enum(["pass", "warn", "fail", "blocked"]),
12211
12392
  synthesis_eligible: z23.boolean()
12212
12393
  });
12394
+ var ClaimIdOnlySchema = z23.object({ claim_id: z23.string() });
12213
12395
  function sha256Bytes(buf) {
12214
12396
  return createHash11("sha256").update(buf).digest("hex");
12215
12397
  }
@@ -12220,11 +12402,15 @@ function readJsonlSafe(filePath) {
12220
12402
  if (!existsSync28(filePath)) return [];
12221
12403
  return parseJsonl(readFileSync(filePath, "utf8"));
12222
12404
  }
12223
- function latestClaimDecisions(reviews) {
12224
- const m = /* @__PURE__ */ new Map();
12225
- const sorted = [...reviews].sort((a, b) => a.created_at.localeCompare(b.created_at));
12226
- for (const r of sorted) m.set(r.claim_id, r.decision);
12227
- return m;
12405
+ function readClaimReviews(filePath) {
12406
+ if (!existsSync28(filePath)) return [];
12407
+ const raw = parseJsonl(readFileSync(filePath, "utf8"));
12408
+ const valid = [];
12409
+ for (const r of raw) {
12410
+ const parsed = ClaimReviewSchema.safeParse(r);
12411
+ if (parsed.success) valid.push(parsed.data);
12412
+ }
12413
+ return valid;
12228
12414
  }
12229
12415
  function latestContradictionStatuses(resolutions) {
12230
12416
  const m = /* @__PURE__ */ new Map();
@@ -12238,7 +12424,7 @@ function latestDispositionStatuses(dispositions) {
12238
12424
  for (const d of sorted) m.set(d.claim_id, "dispositioned");
12239
12425
  return m;
12240
12426
  }
12241
- function deriveManifest(packDir, packageName, operatorNotes = "") {
12427
+ function deriveManifest(packDir, packageName, operatorNotes = "", warnings = []) {
12242
12428
  const yamlPath = join28(packDir, "research.yaml");
12243
12429
  if (!existsSync28(yamlPath)) throw new Error(`research.yaml not found in ${packDir}`);
12244
12430
  const research = ResearchYamlSchema.parse(parseYaml(readFileSync(yamlPath, "utf8")));
@@ -12266,15 +12452,43 @@ function deriveManifest(packDir, packageName, operatorNotes = "") {
12266
12452
  let totalPreserved = 0;
12267
12453
  for (const sectionId of sectionIds) {
12268
12454
  const sectionDir = join28(packDir, "sections", sectionId);
12269
- const reviews = readJsonlSafe(join28(sectionDir, "claim-reviews.jsonl"));
12270
- const decisionMap = latestClaimDecisions(reviews);
12271
- const acceptedCount = [...decisionMap.values()].filter(
12272
- (d) => d === "accepted_for_synthesis"
12273
- ).length;
12455
+ const reviewsPath = join28(sectionDir, "claim-reviews.jsonl");
12456
+ const reviews = readClaimReviews(reviewsPath);
12457
+ const conflicts = findIncompatibleDecisions(reviews);
12458
+ if (conflicts.length > 0) {
12459
+ const first = conflicts[0];
12460
+ throw new Error(
12461
+ `Section ${sectionId}: claim-reviews.jsonl has incompatible decisions for claim_id=${first.claim_id} at created_at=${first.created_at} (decisions seen: ${first.decisions.join(", ")}). Latest-decision-wins tie-breaker undefined \u2014 investigate the review pipeline state.` + (conflicts.length > 1 ? ` (${conflicts.length - 1} other claim_id(s) similarly affected.)` : "")
12462
+ );
12463
+ }
12464
+ const effectiveAccepted = getEffectiveAcceptedClaimIds(reviews);
12465
+ const acceptedCount = effectiveAccepted.size;
12466
+ const claimsPath = join28(sectionDir, "claims.jsonl");
12467
+ if (existsSync28(claimsPath)) {
12468
+ const claimRows = parseJsonl(readFileSync(claimsPath, "utf8"));
12469
+ const claimIds = /* @__PURE__ */ new Set();
12470
+ for (const c of claimRows) {
12471
+ const parsed = ClaimIdOnlySchema.safeParse(c);
12472
+ if (parsed.success) claimIds.add(parsed.data.claim_id);
12473
+ }
12474
+ const phantoms = [];
12475
+ for (const cid of effectiveAccepted) {
12476
+ if (!claimIds.has(cid)) phantoms.push(cid);
12477
+ }
12478
+ if (phantoms.length > 0) {
12479
+ throw new Error(
12480
+ `Section ${sectionId}: ${phantoms.length} effective accepted claim_id(s) absent from claims.jsonl \u2014 phantom claims violate the closure-ledger subset invariant. Examples: ${phantoms.slice(0, 3).join(", ")}` + (phantoms.length > 3 ? ` (+${phantoms.length - 3} more)` : "")
12481
+ );
12482
+ }
12483
+ } else {
12484
+ warnings.push(
12485
+ `section ${sectionId}: claims.jsonl absent \u2014 phantom-claim integrity check skipped`
12486
+ );
12487
+ }
12274
12488
  const auditAccepted = auditSectionMap.get(sectionId);
12275
12489
  if (auditAccepted !== void 0 && auditAccepted !== acceptedCount) {
12276
- throw new Error(
12277
- `Section ${sectionId}: accepted_claims mismatch between claim-reviews.jsonl (${acceptedCount}) and pack-audit.json (${auditAccepted}). Closure-ledger seam disagreement \u2014 investigate before publishing.`
12490
+ warnings.push(
12491
+ `section ${sectionId}: legacy pack-audit.json accepted_claims (${auditAccepted}) differs from effective accepted set (${acceptedCount}). Using effective count (${acceptedCount}) in manifest. Legacy audit count preserved in pack/audits/pack-audit.json (immutable per Law 15).`
12278
12492
  );
12279
12493
  }
12280
12494
  const gateResultPath = join28(packDir, "audits", `${sectionId}-gate.json`);
@@ -12284,6 +12498,11 @@ function deriveManifest(packDir, packageName, operatorNotes = "") {
12284
12498
  const gateResult = GateResultMinimalSchema.parse(
12285
12499
  JSON.parse(readFileSync(gateResultPath, "utf8"))
12286
12500
  );
12501
+ if (!gateResult.synthesis_eligible) {
12502
+ throw new Error(
12503
+ `Section ${sectionId}: gate is not synthesis_eligible (verdict=${gateResult.verdict}). Pack admission requires every section to be synthesis-eligible.`
12504
+ );
12505
+ }
12287
12506
  const dispositions = readJsonlSafe(
12288
12507
  join28(sectionDir, "claim-synthesis-dispositions.jsonl")
12289
12508
  );
@@ -12592,7 +12811,7 @@ async function publish(input) {
12592
12811
  );
12593
12812
  }
12594
12813
  }
12595
- const manifest = deriveManifest(fromDir, packageName, input.operatorNotes ?? "");
12814
+ const manifest = deriveManifest(fromDir, packageName, input.operatorNotes ?? "", warnings);
12596
12815
  if (input.dryRun) {
12597
12816
  const finalReportPath2 = join31(fromDir, "synthesis/final-report.md");
12598
12817
  const finalReport2 = existsSync30(finalReportPath2) ? readFileSync3(finalReportPath2, "utf8") : "";