@mcptoolshop/research-os 0.3.1 → 0.3.3

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
@@ -3627,6 +3627,7 @@ function checkSourceFloor(input) {
3627
3627
  const cfg = input.research.gates.source_floor;
3628
3628
  const results = [];
3629
3629
  const cards = input.sources;
3630
+ const sectionCards = cards.filter((c) => c.section_id === input.section.id);
3630
3631
  const sourceCount = cards.length;
3631
3632
  if (sourceCount < cfg.min_sources) {
3632
3633
  results.push({
@@ -3650,12 +3651,15 @@ function checkSourceFloor(input) {
3650
3651
  const publishers = new Set(
3651
3652
  cards.map((c) => c.publisher).filter((p) => typeof p === "string")
3652
3653
  );
3654
+ const sectionPublishers = new Set(
3655
+ sectionCards.map((c) => c.publisher).filter((p) => typeof p === "string")
3656
+ );
3653
3657
  if (publishers.size < cfg.min_independent_publishers) {
3654
3658
  results.push({
3655
3659
  family: "source_floor",
3656
3660
  check: "min_independent_publishers",
3657
3661
  status: "fail",
3658
- detail: `Found ${publishers.size} independent publisher(s); minimum ${cfg.min_independent_publishers} required.`,
3662
+ detail: `Found ${publishers.size} independent publisher(s); minimum ${cfg.min_independent_publishers} required. (pack-wide=${publishers.size}, section-scoped=${sectionPublishers.size})`,
3659
3663
  evidence: [...publishers],
3660
3664
  blocks_synthesis: true
3661
3665
  });
@@ -3664,18 +3668,19 @@ function checkSourceFloor(input) {
3664
3668
  family: "source_floor",
3665
3669
  check: "min_independent_publishers",
3666
3670
  status: "pass",
3667
- detail: `${publishers.size} independent publisher(s) >= minimum ${cfg.min_independent_publishers}.`,
3671
+ detail: `${publishers.size} independent publisher(s) >= minimum ${cfg.min_independent_publishers}. (pack-wide=${publishers.size}, section-scoped=${sectionPublishers.size})`,
3668
3672
  evidence: [],
3669
3673
  blocks_synthesis: false
3670
3674
  });
3671
3675
  }
3672
3676
  const primaryCount = cards.filter((c) => c.source_type === "primary").length;
3677
+ const sectionPrimaryCount = sectionCards.filter((c) => c.source_type === "primary").length;
3673
3678
  if (primaryCount < cfg.primary_sources_required) {
3674
3679
  results.push({
3675
3680
  family: "source_floor",
3676
3681
  check: "primary_sources_required",
3677
3682
  status: "fail",
3678
- detail: `Found ${primaryCount} primary source(s); minimum ${cfg.primary_sources_required} required. Pre-waiver.`,
3683
+ detail: `Found ${primaryCount} primary source(s); minimum ${cfg.primary_sources_required} required. Pre-waiver. (pack-wide=${primaryCount}, section-scoped=${sectionPrimaryCount})`,
3679
3684
  evidence: cards.filter((c) => c.source_type === "primary").map((c) => c.source_id),
3680
3685
  blocks_synthesis: true
3681
3686
  });
@@ -3684,7 +3689,7 @@ function checkSourceFloor(input) {
3684
3689
  family: "source_floor",
3685
3690
  check: "primary_sources_required",
3686
3691
  status: "pass",
3687
- detail: `${primaryCount} primary source(s) >= minimum ${cfg.primary_sources_required}.`,
3692
+ detail: `${primaryCount} primary source(s) >= minimum ${cfg.primary_sources_required}. (pack-wide=${primaryCount}, section-scoped=${sectionPrimaryCount})`,
3688
3693
  evidence: [],
3689
3694
  blocks_synthesis: false
3690
3695
  });
@@ -3838,25 +3843,14 @@ function checkClaimIntegrity(input) {
3838
3843
  monoClaims.push(claim.claim_id);
3839
3844
  }
3840
3845
  }
3841
- if (monoClaims.length > claims.length / 2) {
3842
- results.push({
3843
- family: "claim_integrity",
3844
- check: "no_source_cluster_monopoly",
3845
- status: "warn",
3846
- detail: `${monoClaims.length}/${claims.length} claim(s) source from a single publisher. Independent corroboration recommended before synthesis.`,
3847
- evidence: monoClaims,
3848
- blocks_synthesis: false
3849
- });
3850
- } else {
3851
- results.push({
3852
- family: "claim_integrity",
3853
- check: "no_source_cluster_monopoly",
3854
- status: "pass",
3855
- detail: `${monoClaims.length}/${claims.length} claim(s) source from a single publisher (within tolerance).`,
3856
- evidence: [],
3857
- blocks_synthesis: false
3858
- });
3859
- }
3846
+ results.push({
3847
+ family: "claim_integrity",
3848
+ check: "no_source_cluster_monopoly",
3849
+ status: "pass",
3850
+ detail: `${monoClaims.length}/${claims.length} claim(s) are single-source by architecture (each claim references exactly one source). Publisher diversity is enforced at source-card level via min_independent_publishers.`,
3851
+ evidence: [],
3852
+ blocks_synthesis: false
3853
+ });
3860
3854
  }
3861
3855
  return results;
3862
3856
  }
@@ -4580,7 +4574,9 @@ var init_schema10 = __esm({
4580
4574
  docs: z11.number().int().nonnegative(),
4581
4575
  unknown: z11.number().int().nonnegative(),
4582
4576
  independent_publishers: z11.number().int().nonnegative(),
4583
- failed_fetches: z11.number().int().nonnegative()
4577
+ failed_fetches: z11.number().int().nonnegative(),
4578
+ section_primary: z11.number().int().nonnegative(),
4579
+ section_independent_publishers: z11.number().int().nonnegative()
4584
4580
  }),
4585
4581
  contradiction_counts: z11.object({
4586
4582
  total: z11.number().int().nonnegative(),
@@ -4653,10 +4649,14 @@ function summarizeClaimCounts(input) {
4653
4649
  }
4654
4650
  function summarizeSourceCounts(input) {
4655
4651
  const cards = input.sources;
4652
+ const sectionCards = cards.filter((c) => c.section_id === input.section.id);
4656
4653
  const failed = input.receipts.filter((r) => r.fetch_outcome !== "ok").length;
4657
4654
  const publishers = new Set(
4658
4655
  cards.map((c) => c.publisher).filter((p) => typeof p === "string")
4659
4656
  );
4657
+ const sectionPublishers = new Set(
4658
+ sectionCards.map((c) => c.publisher).filter((p) => typeof p === "string")
4659
+ );
4660
4660
  return {
4661
4661
  total: cards.length,
4662
4662
  primary: cards.filter((c) => c.source_type === "primary").length,
@@ -4666,7 +4666,9 @@ function summarizeSourceCounts(input) {
4666
4666
  docs: cards.filter((c) => c.source_type === "docs").length,
4667
4667
  unknown: cards.filter((c) => c.source_type === "unknown").length,
4668
4668
  independent_publishers: publishers.size,
4669
- failed_fetches: failed
4669
+ failed_fetches: failed,
4670
+ section_primary: sectionCards.filter((c) => c.source_type === "primary").length,
4671
+ section_independent_publishers: sectionPublishers.size
4670
4672
  };
4671
4673
  }
4672
4674
  function buildEffectiveStatuses3(resolutions) {
@@ -11872,7 +11874,7 @@ var init_src = __esm({
11872
11874
  init_triage();
11873
11875
  init_discover();
11874
11876
  init_errors();
11875
- RESEARCH_OS_VERSION = "0.3.1";
11877
+ RESEARCH_OS_VERSION = "0.3.3";
11876
11878
  }
11877
11879
  });
11878
11880
 
@@ -12311,12 +12313,55 @@ import { join as join31, basename, resolve as resolve24 } from "path";
12311
12313
 
12312
12314
  // src/pack/publish/manifest.ts
12313
12315
  init_schema();
12316
+ init_schema9();
12314
12317
  import { createHash as createHash11 } from "crypto";
12315
12318
  import { readFileSync, existsSync as existsSync28 } from "fs";
12316
12319
  import { join as join28 } from "path";
12317
12320
  import { parse as parseYaml } from "yaml";
12318
12321
  import { z as z23 } from "zod";
12319
12322
 
12323
+ // src/closure-ledger/effective-accepted.ts
12324
+ function getEffectiveDecisionMap(reviews) {
12325
+ const map2 = /* @__PURE__ */ new Map();
12326
+ for (const r of reviews) {
12327
+ const existing = map2.get(r.claim_id);
12328
+ if (!existing || r.created_at > existing.created_at) {
12329
+ map2.set(r.claim_id, r);
12330
+ }
12331
+ }
12332
+ return map2;
12333
+ }
12334
+ function getEffectiveAcceptedClaimIds(reviews) {
12335
+ const decisionMap = getEffectiveDecisionMap(reviews);
12336
+ const accepted = /* @__PURE__ */ new Set();
12337
+ for (const [claim_id, r] of decisionMap) {
12338
+ if (r.decision === "accepted_for_synthesis") accepted.add(claim_id);
12339
+ }
12340
+ return accepted;
12341
+ }
12342
+ function findIncompatibleDecisions(reviews) {
12343
+ const groups = /* @__PURE__ */ new Map();
12344
+ for (const r of reviews) {
12345
+ const key = `${r.claim_id}|${r.created_at}`;
12346
+ let set = groups.get(key);
12347
+ if (!set) {
12348
+ set = /* @__PURE__ */ new Set();
12349
+ groups.set(key, set);
12350
+ }
12351
+ set.add(r.decision);
12352
+ }
12353
+ const conflicts = [];
12354
+ for (const [key, decisions] of groups) {
12355
+ if (decisions.size > 1) {
12356
+ const sep3 = key.lastIndexOf("|");
12357
+ const claim_id = key.slice(0, sep3);
12358
+ const created_at = key.slice(sep3 + 1);
12359
+ conflicts.push({ claim_id, created_at, decisions: [...decisions] });
12360
+ }
12361
+ }
12362
+ return conflicts;
12363
+ }
12364
+
12320
12365
  // src/pack/publish/schema.ts
12321
12366
  import { z as z22 } from "zod";
12322
12367
  var SectionSummarySchema = z22.object({
@@ -12348,6 +12393,7 @@ var GateResultMinimalSchema = z23.object({
12348
12393
  verdict: z23.enum(["pass", "warn", "fail", "blocked"]),
12349
12394
  synthesis_eligible: z23.boolean()
12350
12395
  });
12396
+ var ClaimIdOnlySchema = z23.object({ claim_id: z23.string() });
12351
12397
  function sha256Bytes(buf) {
12352
12398
  return createHash11("sha256").update(buf).digest("hex");
12353
12399
  }
@@ -12358,11 +12404,15 @@ function readJsonlSafe(filePath) {
12358
12404
  if (!existsSync28(filePath)) return [];
12359
12405
  return parseJsonl(readFileSync(filePath, "utf8"));
12360
12406
  }
12361
- function latestClaimDecisions(reviews) {
12362
- const m = /* @__PURE__ */ new Map();
12363
- const sorted = [...reviews].sort((a, b) => a.created_at.localeCompare(b.created_at));
12364
- for (const r of sorted) m.set(r.claim_id, r.decision);
12365
- return m;
12407
+ function readClaimReviews(filePath) {
12408
+ if (!existsSync28(filePath)) return [];
12409
+ const raw = parseJsonl(readFileSync(filePath, "utf8"));
12410
+ const valid = [];
12411
+ for (const r of raw) {
12412
+ const parsed = ClaimReviewSchema.safeParse(r);
12413
+ if (parsed.success) valid.push(parsed.data);
12414
+ }
12415
+ return valid;
12366
12416
  }
12367
12417
  function latestContradictionStatuses(resolutions) {
12368
12418
  const m = /* @__PURE__ */ new Map();
@@ -12376,7 +12426,7 @@ function latestDispositionStatuses(dispositions) {
12376
12426
  for (const d of sorted) m.set(d.claim_id, "dispositioned");
12377
12427
  return m;
12378
12428
  }
12379
- function deriveManifest(packDir, packageName, operatorNotes = "") {
12429
+ function deriveManifest(packDir, packageName, operatorNotes = "", warnings = []) {
12380
12430
  const yamlPath = join28(packDir, "research.yaml");
12381
12431
  if (!existsSync28(yamlPath)) throw new Error(`research.yaml not found in ${packDir}`);
12382
12432
  const research = ResearchYamlSchema.parse(parseYaml(readFileSync(yamlPath, "utf8")));
@@ -12404,15 +12454,43 @@ function deriveManifest(packDir, packageName, operatorNotes = "") {
12404
12454
  let totalPreserved = 0;
12405
12455
  for (const sectionId of sectionIds) {
12406
12456
  const sectionDir = join28(packDir, "sections", sectionId);
12407
- const reviews = readJsonlSafe(join28(sectionDir, "claim-reviews.jsonl"));
12408
- const decisionMap = latestClaimDecisions(reviews);
12409
- const acceptedCount = [...decisionMap.values()].filter(
12410
- (d) => d === "accepted_for_synthesis"
12411
- ).length;
12457
+ const reviewsPath = join28(sectionDir, "claim-reviews.jsonl");
12458
+ const reviews = readClaimReviews(reviewsPath);
12459
+ const conflicts = findIncompatibleDecisions(reviews);
12460
+ if (conflicts.length > 0) {
12461
+ const first = conflicts[0];
12462
+ throw new Error(
12463
+ `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.)` : "")
12464
+ );
12465
+ }
12466
+ const effectiveAccepted = getEffectiveAcceptedClaimIds(reviews);
12467
+ const acceptedCount = effectiveAccepted.size;
12468
+ const claimsPath = join28(sectionDir, "claims.jsonl");
12469
+ if (existsSync28(claimsPath)) {
12470
+ const claimRows = parseJsonl(readFileSync(claimsPath, "utf8"));
12471
+ const claimIds = /* @__PURE__ */ new Set();
12472
+ for (const c of claimRows) {
12473
+ const parsed = ClaimIdOnlySchema.safeParse(c);
12474
+ if (parsed.success) claimIds.add(parsed.data.claim_id);
12475
+ }
12476
+ const phantoms = [];
12477
+ for (const cid of effectiveAccepted) {
12478
+ if (!claimIds.has(cid)) phantoms.push(cid);
12479
+ }
12480
+ if (phantoms.length > 0) {
12481
+ throw new Error(
12482
+ `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)` : "")
12483
+ );
12484
+ }
12485
+ } else {
12486
+ warnings.push(
12487
+ `section ${sectionId}: claims.jsonl absent \u2014 phantom-claim integrity check skipped`
12488
+ );
12489
+ }
12412
12490
  const auditAccepted = auditSectionMap.get(sectionId);
12413
12491
  if (auditAccepted !== void 0 && auditAccepted !== acceptedCount) {
12414
- throw new Error(
12415
- `Section ${sectionId}: accepted_claims mismatch between claim-reviews.jsonl (${acceptedCount}) and pack-audit.json (${auditAccepted}). Closure-ledger seam disagreement \u2014 investigate before publishing.`
12492
+ warnings.push(
12493
+ `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).`
12416
12494
  );
12417
12495
  }
12418
12496
  const gateResultPath = join28(packDir, "audits", `${sectionId}-gate.json`);
@@ -12422,6 +12500,11 @@ function deriveManifest(packDir, packageName, operatorNotes = "") {
12422
12500
  const gateResult = GateResultMinimalSchema.parse(
12423
12501
  JSON.parse(readFileSync(gateResultPath, "utf8"))
12424
12502
  );
12503
+ if (!gateResult.synthesis_eligible) {
12504
+ throw new Error(
12505
+ `Section ${sectionId}: gate is not synthesis_eligible (verdict=${gateResult.verdict}). Pack admission requires every section to be synthesis-eligible.`
12506
+ );
12507
+ }
12425
12508
  const dispositions = readJsonlSafe(
12426
12509
  join28(sectionDir, "claim-synthesis-dispositions.jsonl")
12427
12510
  );
@@ -12730,7 +12813,7 @@ async function publish(input) {
12730
12813
  );
12731
12814
  }
12732
12815
  }
12733
- const manifest = deriveManifest(fromDir, packageName, input.operatorNotes ?? "");
12816
+ const manifest = deriveManifest(fromDir, packageName, input.operatorNotes ?? "", warnings);
12734
12817
  if (input.dryRun) {
12735
12818
  const finalReportPath2 = join31(fromDir, "synthesis/final-report.md");
12736
12819
  const finalReport2 = existsSync30(finalReportPath2) ? readFileSync3(finalReportPath2, "utf8") : "";