@mcptoolshop/research-os 0.1.1 → 0.3.0

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
@@ -2774,9 +2774,6 @@ Hard rules:
2774
2774
  });
2775
2775
 
2776
2776
  // src/contradictions/detectors/index.ts
2777
- function defaultContradictionDetectors() {
2778
- return [new OllamaInternContradictionDetector(), new HeuristicContradictionDetector()];
2779
- }
2780
2777
  async function pickContradictionDetector(detectors) {
2781
2778
  for (const d of detectors) {
2782
2779
  if (await d.available()) return d;
@@ -3250,6 +3247,48 @@ function buildContradiction(args) {
3250
3247
  created_at: (/* @__PURE__ */ new Date()).toISOString()
3251
3248
  });
3252
3249
  }
3250
+ async function resolveDetector(options) {
3251
+ const mode = options.detectorMode ?? "auto";
3252
+ if (!VALID_DETECTOR_MODES.includes(mode)) {
3253
+ throw new Error(
3254
+ `contradict map: invalid --detector value "${mode}"; valid values are: auto, heuristic, ollama-intern`
3255
+ );
3256
+ }
3257
+ if (mode === "heuristic") {
3258
+ return {
3259
+ detector: new HeuristicContradictionDetector(),
3260
+ announcement: "contradict map: using heuristic detector"
3261
+ };
3262
+ }
3263
+ if (mode === "ollama-intern") {
3264
+ const d = new OllamaInternContradictionDetector(options.ollamaConfig ?? {});
3265
+ if (!await d.available()) {
3266
+ throw new Error(
3267
+ `contradict map: ollama-intern detector requested but model ${d.model} is unavailable; aborting (use --detector heuristic to bypass)`
3268
+ );
3269
+ }
3270
+ return {
3271
+ detector: d,
3272
+ announcement: `contradict map: using ollama-intern detector with model ${d.model}`
3273
+ };
3274
+ }
3275
+ const detectors = options.detectors ?? [
3276
+ new OllamaInternContradictionDetector(options.ollamaConfig ?? {}),
3277
+ new HeuristicContradictionDetector()
3278
+ ];
3279
+ const detector = await pickContradictionDetector(detectors);
3280
+ if (detector.name === "ollama-intern") {
3281
+ const modelName = detector instanceof OllamaInternContradictionDetector ? detector.model : process.env.OLLAMA_INTERN_MODEL ?? "hermes3:8b";
3282
+ return {
3283
+ detector,
3284
+ announcement: `contradict map: using ollama-intern detector with model ${modelName}`
3285
+ };
3286
+ }
3287
+ return {
3288
+ detector,
3289
+ announcement: "contradict map: ollama-intern unavailable; using heuristic detector"
3290
+ };
3291
+ }
3253
3292
  async function map(options) {
3254
3293
  const packPath = options.packPath ? resolve7(options.packPath) : process.cwd();
3255
3294
  if (!existsSync9(join10(packPath, "research.yaml"))) throw new PackNotFoundError(packPath);
@@ -3261,8 +3300,7 @@ async function map(options) {
3261
3300
  const allowed = await readTriagedClaimIds2(packPath, options.sectionId);
3262
3301
  candidateClaims = candidateClaims.filter((c) => allowed.has(c.claim_id));
3263
3302
  }
3264
- const adapters = options.detectors ?? defaultContradictionDetectors();
3265
- const detector = await pickContradictionDetector(adapters);
3303
+ const { detector, announcement } = await resolveDetector(options);
3266
3304
  const summary = {
3267
3305
  sectionId: options.sectionId,
3268
3306
  detector: detector.name,
@@ -3272,7 +3310,8 @@ async function map(options) {
3272
3310
  contradictionsAdded: 0,
3273
3311
  contradictionsDeduped: 0,
3274
3312
  contradictionIds: [],
3275
- detectorError: null
3313
+ detectorError: null,
3314
+ detectorAnnouncement: announcement
3276
3315
  };
3277
3316
  const ledgerPath = join10(sectionDir, "contradictions.jsonl");
3278
3317
  const mdPath = join10(sectionDir, "contradictions.md");
@@ -3336,7 +3375,7 @@ async function map(options) {
3336
3375
  await writeFile9(mdPath, md, "utf8");
3337
3376
  return summary;
3338
3377
  }
3339
- var DETECTOR_ID_PART;
3378
+ var DETECTOR_ID_PART, VALID_DETECTOR_MODES;
3340
3379
  var init_map = __esm({
3341
3380
  "src/contradictions/map.ts"() {
3342
3381
  "use strict";
@@ -3344,11 +3383,14 @@ var init_map = __esm({
3344
3383
  init_schema5();
3345
3384
  init_schema7();
3346
3385
  init_detectors();
3386
+ init_heuristic3();
3387
+ init_ollama_intern3();
3347
3388
  init_markdown();
3348
3389
  DETECTOR_ID_PART = {
3349
3390
  heuristic: "heuristic",
3350
3391
  "ollama-intern": "ollama_intern"
3351
3392
  };
3393
+ VALID_DETECTOR_MODES = ["auto", "heuristic", "ollama-intern"];
3352
3394
  }
3353
3395
  });
3354
3396
 
@@ -7265,6 +7307,7 @@ function buildSectionState(args) {
7265
7307
  candidate_claims_total: candidateClaims.length,
7266
7308
  unresolved_contradiction_ids: unresolved.map((c) => c.contradiction_id),
7267
7309
  blocking_reasons: gate2?.blocking_reasons ?? [],
7310
+ active_blockers: gate2?.blocking_reasons ?? [],
7268
7311
  blocking_contradictions_unresolved: blocking.length,
7269
7312
  provenance_summary: provenanceSummary
7270
7313
  };
@@ -7308,7 +7351,7 @@ function determineMode(sections, waivers, warnings) {
7308
7351
  return "human_review_required";
7309
7352
  }
7310
7353
  const allReady = sections.every(
7311
- (s) => s.has_gate_run && s.synthesis_eligible && s.has_review_run && s.candidate_claims_total > 0 && s.repair_claim_ids.length === 0 && s.unresolved_contradiction_ids.length === 0
7354
+ (s) => s.has_gate_run && s.synthesis_eligible && s.has_review_run && s.candidate_claims_total > 0 && s.active_blockers.length === 0 && s.unresolved_contradiction_ids.length === 0
7312
7355
  );
7313
7356
  if (allReady && sections.length > 0) return "synthesis_ready";
7314
7357
  return "repair_required";
@@ -7517,6 +7560,7 @@ var init_schema13 = __esm({
7517
7560
  candidate_claims_total: z14.number().int().nonnegative(),
7518
7561
  unresolved_contradiction_ids: z14.array(z14.string()),
7519
7562
  blocking_reasons: z14.array(z14.string()),
7563
+ active_blockers: z14.array(z14.string()).default([]),
7520
7564
  blocking_contradictions_unresolved: z14.number().int().nonnegative(),
7521
7565
  provenance_summary: ProvenanceSummarySchema.optional()
7522
7566
  });
@@ -9133,7 +9177,7 @@ function buildReadinessSummary(rows, handoff2, unresolvedContradictions) {
9133
9177
  if (!r.has_gate_run) noGate += 1;
9134
9178
  if (!r.has_review_run) noReview += 1;
9135
9179
  const unresolvedCount = unresolvedBySection.get(r.section_id) ?? 0;
9136
- if (r.synthesis_eligible && r.has_review_run && r.candidate_claims > 0 && r.repair_claims === 0 && unresolvedCount === 0) {
9180
+ if (r.synthesis_eligible && r.has_review_run && r.candidate_claims > 0 && r.blocking_reasons.length === 0 && unresolvedCount === 0) {
9137
9181
  ready += 1;
9138
9182
  } else if (r.has_gate_run && r.gate_verdict === "blocked" && !r.synthesis_eligible) {
9139
9183
  blocked += 1;
@@ -11690,7 +11734,7 @@ var init_src = __esm({
11690
11734
  init_triage();
11691
11735
  init_discover();
11692
11736
  init_errors();
11693
- RESEARCH_OS_VERSION = "0.1.0";
11737
+ RESEARCH_OS_VERSION = "0.3.0";
11694
11738
  }
11695
11739
  });
11696
11740
 
@@ -12121,9 +12165,507 @@ init_synth();
12121
12165
  init_audit();
12122
12166
  init_freeze();
12123
12167
  init_invalidate();
12168
+ import { Command, Option } from "commander";
12169
+
12170
+ // src/pack/publish/index.ts
12171
+ import { existsSync as existsSync30, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
12172
+ import { join as join31, basename, resolve as resolve24 } from "path";
12173
+
12174
+ // src/pack/publish/manifest.ts
12175
+ init_schema();
12176
+ import { createHash as createHash11 } from "crypto";
12177
+ import { readFileSync, existsSync as existsSync28 } from "fs";
12178
+ import { join as join28 } from "path";
12179
+ import { parse as parseYaml } from "yaml";
12180
+ import { z as z23 } from "zod";
12181
+
12182
+ // src/pack/publish/schema.ts
12183
+ import { z as z22 } from "zod";
12184
+ var SectionSummarySchema = z22.object({
12185
+ id: z22.string().min(1),
12186
+ accepted_claims: z22.number().int().min(0),
12187
+ gate: z22.enum(["pass", "warn", "fail", "blocked", "pass_with_waiver"]),
12188
+ synthesis_eligible: z22.boolean()
12189
+ });
12190
+ var TotalsSchema = z22.object({
12191
+ sections: z22.number().int().min(1),
12192
+ accepted_claims: z22.number().int().min(0),
12193
+ dispositioned: z22.number().int().min(0),
12194
+ unresolved_contradictions: z22.number().int().min(0),
12195
+ preserved_contradiction_records: z22.number().int().min(0).optional()
12196
+ });
12197
+ var PackManifestSchema = z22.object({
12198
+ name: z22.string().min(1),
12199
+ topic: z22.string().min(1),
12200
+ frozen_at: z22.string().datetime(),
12201
+ research_os_version: z22.string().min(1),
12202
+ sections: z22.array(SectionSummarySchema).min(1),
12203
+ totals: TotalsSchema,
12204
+ freeze_receipt_sha256: z22.string().regex(/^[a-f0-9]{64}$/, "Must be a 64-char hex sha256"),
12205
+ operator_notes: z22.string().default("")
12206
+ });
12207
+
12208
+ // src/pack/publish/manifest.ts
12209
+ var GateResultMinimalSchema = z23.object({
12210
+ verdict: z23.enum(["pass", "warn", "fail", "blocked"]),
12211
+ synthesis_eligible: z23.boolean()
12212
+ });
12213
+ function sha256Bytes(buf) {
12214
+ return createHash11("sha256").update(buf).digest("hex");
12215
+ }
12216
+ function parseJsonl(content) {
12217
+ return content.split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
12218
+ }
12219
+ function readJsonlSafe(filePath) {
12220
+ if (!existsSync28(filePath)) return [];
12221
+ return parseJsonl(readFileSync(filePath, "utf8"));
12222
+ }
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;
12228
+ }
12229
+ function latestContradictionStatuses(resolutions) {
12230
+ const m = /* @__PURE__ */ new Map();
12231
+ const sorted = [...resolutions].sort((a, b) => a.resolved_at.localeCompare(b.resolved_at));
12232
+ for (const r of sorted) m.set(r.contradiction_id, r.status);
12233
+ return m;
12234
+ }
12235
+ function latestDispositionStatuses(dispositions) {
12236
+ const m = /* @__PURE__ */ new Map();
12237
+ const sorted = [...dispositions].sort((a, b) => a.created_at.localeCompare(b.created_at));
12238
+ for (const d of sorted) m.set(d.claim_id, "dispositioned");
12239
+ return m;
12240
+ }
12241
+ function deriveManifest(packDir, packageName, operatorNotes = "") {
12242
+ const yamlPath = join28(packDir, "research.yaml");
12243
+ if (!existsSync28(yamlPath)) throw new Error(`research.yaml not found in ${packDir}`);
12244
+ const research = ResearchYamlSchema.parse(parseYaml(readFileSync(yamlPath, "utf8")));
12245
+ if (!research.frozen_at) {
12246
+ throw new Error(`Pack is not frozen: research.yaml.frozen_at is null \u2014 run research-os freeze first`);
12247
+ }
12248
+ const receiptPath = join28(packDir, "audits/freeze-receipt.json");
12249
+ if (!existsSync28(receiptPath)) {
12250
+ throw new Error(`audits/freeze-receipt.json not found \u2014 pack is not frozen`);
12251
+ }
12252
+ const receiptBytes = readFileSync(receiptPath);
12253
+ const freeze_receipt_sha256 = sha256Bytes(receiptBytes);
12254
+ const receipt = JSON.parse(receiptBytes.toString("utf8"));
12255
+ const frozen_at = receipt.frozen_at ?? research.frozen_at;
12256
+ const packAuditPath = join28(packDir, "audits/pack-audit.json");
12257
+ if (!existsSync28(packAuditPath)) throw new Error(`audits/pack-audit.json not found`);
12258
+ const packAudit = JSON.parse(readFileSync(packAuditPath, "utf8"));
12259
+ const auditSectionMap = new Map(
12260
+ (packAudit.section_summaries ?? []).map((s) => [s.section_id, s.accepted_claims])
12261
+ );
12262
+ const sectionIds = research.sections.map((s) => s.id);
12263
+ const sections = [];
12264
+ let totalAccepted = 0;
12265
+ let totalDispositioned = 0;
12266
+ let totalPreserved = 0;
12267
+ for (const sectionId of sectionIds) {
12268
+ 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;
12274
+ const auditAccepted = auditSectionMap.get(sectionId);
12275
+ 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.`
12278
+ );
12279
+ }
12280
+ const gateResultPath = join28(packDir, "audits", `${sectionId}-gate.json`);
12281
+ if (!existsSync28(gateResultPath)) {
12282
+ throw new Error(`audits/${sectionId}-gate.json not found \u2014 section not gated`);
12283
+ }
12284
+ const gateResult = GateResultMinimalSchema.parse(
12285
+ JSON.parse(readFileSync(gateResultPath, "utf8"))
12286
+ );
12287
+ const dispositions = readJsonlSafe(
12288
+ join28(sectionDir, "claim-synthesis-dispositions.jsonl")
12289
+ );
12290
+ const dispositionMap = latestDispositionStatuses(dispositions);
12291
+ totalDispositioned += dispositionMap.size;
12292
+ const resolutions = readJsonlSafe(
12293
+ join28(sectionDir, "contradiction-resolutions.jsonl")
12294
+ );
12295
+ const resolutionMap = latestContradictionStatuses(resolutions);
12296
+ const stillUnresolved = [...resolutionMap.values()].filter((s) => s === "unresolved").length;
12297
+ if (stillUnresolved > 0) {
12298
+ throw new Error(
12299
+ `Section ${sectionId} has ${stillUnresolved} unresolved contradictions. Freeze should have blocked this \u2014 investigate before publishing.`
12300
+ );
12301
+ }
12302
+ totalPreserved += [...resolutionMap.values()].filter((s) => s !== "unresolved").length;
12303
+ totalAccepted += acceptedCount;
12304
+ sections.push({
12305
+ id: sectionId,
12306
+ accepted_claims: acceptedCount,
12307
+ gate: gateResult.verdict,
12308
+ synthesis_eligible: gateResult.synthesis_eligible
12309
+ });
12310
+ }
12311
+ const totalsBase = {
12312
+ sections: sections.length,
12313
+ accepted_claims: totalAccepted,
12314
+ dispositioned: totalDispositioned,
12315
+ unresolved_contradictions: 0
12316
+ };
12317
+ return PackManifestSchema.parse({
12318
+ name: packageName,
12319
+ topic: research.topic,
12320
+ frozen_at,
12321
+ research_os_version: research.research_os_version,
12322
+ sections,
12323
+ totals: totalPreserved > 0 ? { ...totalsBase, preserved_contradiction_records: totalPreserved } : totalsBase,
12324
+ freeze_receipt_sha256,
12325
+ operator_notes: operatorNotes
12326
+ });
12327
+ }
12328
+
12329
+ // src/pack/publish/readme.ts
12330
+ function extractSummary(markdown) {
12331
+ const lines = markdown.split("\n");
12332
+ let inSummary = false;
12333
+ const summaryLines = [];
12334
+ for (const line of lines) {
12335
+ if (/^## Summary\s*$/.test(line)) {
12336
+ inSummary = true;
12337
+ continue;
12338
+ }
12339
+ if (inSummary && /^## /.test(line)) break;
12340
+ if (inSummary) summaryLines.push(line);
12341
+ }
12342
+ return summaryLines.join("\n").trim();
12343
+ }
12344
+ function generateReadme(manifest, finalReport) {
12345
+ const m = manifest;
12346
+ const frozenDate = m.frozen_at.slice(0, 10);
12347
+ const summary = extractSummary(finalReport);
12348
+ const sectionTable = m.sections.map((s) => `| ${s.id} | ${s.accepted_claims} | ${s.gate} | ${s.synthesis_eligible ? "yes" : "no"} |`).join("\n");
12349
+ const totalsLine = m.totals.preserved_contradiction_records != null ? `Preserved contradiction records: ${m.totals.preserved_contradiction_records}` : `${m.totals.unresolved_contradictions} unresolved contradictions`;
12350
+ const operatorSection = m.operator_notes ? `
12351
+ ---
12352
+
12353
+ ## Operator notes
12354
+
12355
+ ${m.operator_notes}
12356
+ ` : "";
12357
+ return `# ${m.name}
12358
+
12359
+ **Topic:** ${m.topic}
12360
+
12361
+ **Frozen:** ${frozenDate} | **research-os version:** ${m.research_os_version} | **Accepted claims:** ${m.totals.accepted_claims} across ${m.totals.sections} sections
12362
+
12363
+ ---
12364
+
12365
+ ## Executive summary
12366
+
12367
+ ${summary}
12368
+
12369
+ ---
12370
+
12371
+ ## Sections
12372
+
12373
+ | Section | Accepted claims | Gate | Synthesis eligible |
12374
+ |---------|-----------------|------|-------------------|
12375
+ ${sectionTable}
12376
+
12377
+ **Totals:** ${m.totals.accepted_claims} accepted, ${m.totals.dispositioned} dispositioned, ${totalsLine}
12378
+
12379
+ ---
12380
+
12381
+ ## How to read this pack
12382
+
12383
+ This package is part of the [\`research-packs\`](../../README.md) archive.
12384
+
12385
+ - **Lane 1 (synthesis):** You are here. See [\`synthesis/final-report.md\`](synthesis/final-report.md) for the full citation-clean prose.
12386
+ - **Lane 2 (evidence):** [\`pack/\`](pack/) \u2014 full frozen ledgers, source cards, excerpts, claim reviews, gate results, and \`audits/freeze-receipt.json\`.
12387
+ - **Lane 3 (method):** [\`../../docs/\`](../../docs/) \u2014 artifact contract, how-to-read, source quality notes.
12388
+
12389
+ To verify this pack's integrity: \`node ../../scripts/verify-pack.mjs .\` from this directory.
12390
+
12391
+ See [\`docs/how-to-read-this.md\`](docs/how-to-read-this.md) for pack-specific reading notes.${operatorSection}`;
12392
+ }
12393
+
12394
+ // src/pack/publish/how-to-read.ts
12395
+ function generateHowToReadScaffold(manifest) {
12396
+ const frozenDate = manifest.frozen_at.slice(0, 10);
12397
+ const contradictionNote = manifest.totals.preserved_contradiction_records != null ? `This pack has ${manifest.totals.preserved_contradiction_records} preserved contradiction records. These are not active blockers \u2014 freeze validated zero unresolved contradictions across all sections. The records are preserved for provenance in \`pack/sections/<id>/contradiction-resolutions.jsonl\`.` : "This pack has no preserved contradiction records.";
12398
+ return `# How to read: ${manifest.name}
12399
+
12400
+ <!-- SCAFFOLD: Pre-filled with pack-specific metadata by \`research-os pack publish\`.
12401
+ Final prose is human-authored. Edit freely \u2014 this file is NOT covered by the freeze receipt. -->
12402
+
12403
+ **Pack:** \`${manifest.name}\`
12404
+ **Topic:** ${manifest.topic}
12405
+ **Frozen:** ${frozenDate}
12406
+ **Accepted claims:** ${manifest.totals.accepted_claims} accepted claims across ${manifest.totals.sections} section${manifest.totals.sections !== 1 ? "s" : ""}
12407
+
12408
+ ---
12409
+
12410
+ ## What this pack answers
12411
+
12412
+ <!-- human-authored: describe what the synthesis delivers -->
12413
+
12414
+ ## How the evidence is structured
12415
+
12416
+ <!-- human-authored: explain section breakdown, what each section investigated -->
12417
+
12418
+ ## Interpreting claim IDs
12419
+
12420
+ Claims are referenced as \`[claim:clm_<hex>]\` in the synthesis prose. To look up a claim:
12421
+
12422
+ 1. Open \`pack/sections/<section-id>/claims.jsonl\`
12423
+ 2. Find the entry with matching \`claim_id\`
12424
+ 3. The \`asserts\` field is the claim; \`evidence_excerpt\` is the literal source span
12425
+
12426
+ Accepted vs rejected: \`pack/sections/<section-id>/claim-reviews.jsonl\` \u2014 latest entry per \`claim_id\` wins.
12427
+
12428
+ ## Preserved contradiction records
12429
+
12430
+ ${contradictionNote}
12431
+
12432
+ ## Verifying integrity
12433
+
12434
+ From this directory:
12435
+
12436
+ \`\`\`bash
12437
+ node ../../scripts/verify-pack.mjs .
12438
+ \`\`\`
12439
+
12440
+ Expected output: PASS with artifact count and receipt sha256.
12441
+
12442
+ See [\`../../docs/how-to-read-a-pack.md\`](../../docs/how-to-read-a-pack.md) for the general guide.
12443
+ `;
12444
+ }
12445
+
12446
+ // src/pack/publish/copy.ts
12447
+ import { mkdirSync as mkdirSync2, copyFileSync, readdirSync } from "fs";
12448
+ import { join as join29 } from "path";
12449
+ function copyDir(src, dst) {
12450
+ mkdirSync2(dst, { recursive: true });
12451
+ let count = 0;
12452
+ const entries = readdirSync(src, { withFileTypes: true });
12453
+ for (const entry of entries) {
12454
+ const srcPath = join29(src, entry.name);
12455
+ const dstPath = join29(dst, entry.name);
12456
+ if (entry.isDirectory()) {
12457
+ count += copyDir(srcPath, dstPath);
12458
+ } else if (entry.isFile()) {
12459
+ copyFileSync(srcPath, dstPath);
12460
+ count++;
12461
+ }
12462
+ }
12463
+ return count;
12464
+ }
12465
+
12466
+ // src/pack/publish/verify.ts
12467
+ import { createHash as createHash12 } from "crypto";
12468
+ import { readFileSync as readFileSync2, existsSync as existsSync29 } from "fs";
12469
+ import { join as join30 } from "path";
12470
+ var REQUIRED_FILES = [
12471
+ "pack/audits/freeze-receipt.json",
12472
+ "synthesis/final-report.md",
12473
+ "synthesis/decision-brief.md",
12474
+ "pack.manifest.json",
12475
+ "README.md"
12476
+ ];
12477
+ function sha256File(filePath) {
12478
+ return createHash12("sha256").update(readFileSync2(filePath)).digest("hex");
12479
+ }
12480
+ function verifyPack(packageDir) {
12481
+ for (const rel of REQUIRED_FILES) {
12482
+ const full = join30(packageDir, rel);
12483
+ if (!existsSync29(full)) {
12484
+ return { pass: false, reason: `MISSING required file: ${rel}` };
12485
+ }
12486
+ }
12487
+ let rawManifest;
12488
+ try {
12489
+ rawManifest = JSON.parse(readFileSync2(join30(packageDir, "pack.manifest.json"), "utf8"));
12490
+ } catch (e) {
12491
+ return { pass: false, reason: `pack.manifest.json parse error: ${e.message}` };
12492
+ }
12493
+ const parsed = PackManifestSchema.safeParse(rawManifest);
12494
+ if (!parsed.success) {
12495
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
12496
+ return { pass: false, reason: `pack.manifest.json schema violation: ${issues}` };
12497
+ }
12498
+ const m = parsed.data;
12499
+ const receiptPath = join30(packageDir, "pack/audits/freeze-receipt.json");
12500
+ const actualReceiptHash = sha256File(receiptPath);
12501
+ if (actualReceiptHash !== m.freeze_receipt_sha256) {
12502
+ return {
12503
+ pass: false,
12504
+ reason: `freeze-receipt.json hash mismatch.
12505
+ manifest: ${m.freeze_receipt_sha256}
12506
+ actual: ${actualReceiptHash}`,
12507
+ name: m.name
12508
+ };
12509
+ }
12510
+ let receipt;
12511
+ try {
12512
+ receipt = JSON.parse(readFileSync2(receiptPath, "utf8"));
12513
+ } catch (e) {
12514
+ return {
12515
+ pass: false,
12516
+ reason: `freeze-receipt.json parse error: ${e.message}`,
12517
+ name: m.name
12518
+ };
12519
+ }
12520
+ const allFingerprints = [
12521
+ ...receipt.canonical_artifact_hashes ?? [],
12522
+ ...receipt.synthesis_hashes ?? []
12523
+ ];
12524
+ let verified = 0;
12525
+ const softWarnings = [];
12526
+ for (const entry of allFingerprints) {
12527
+ const artifactPath = join30(packageDir, "pack", entry.path);
12528
+ if (!existsSync29(artifactPath)) {
12529
+ return {
12530
+ pass: false,
12531
+ reason: `Fingerprinted artifact missing: pack/${entry.path}`,
12532
+ name: m.name
12533
+ };
12534
+ }
12535
+ const actualHash = sha256File(artifactPath);
12536
+ if (actualHash !== entry.sha256) {
12537
+ if (entry.path === "research.yaml") {
12538
+ softWarnings.push(
12539
+ `WARN pack/research.yaml hash reflects pre-freeze state (known: freeze writes frozen_at + status after fingerprinting)`
12540
+ );
12541
+ verified++;
12542
+ continue;
12543
+ }
12544
+ return {
12545
+ pass: false,
12546
+ reason: `Hash mismatch for pack/${entry.path}.
12547
+ receipt: ${entry.sha256}
12548
+ actual: ${actualHash}`,
12549
+ name: m.name
12550
+ };
12551
+ }
12552
+ verified++;
12553
+ }
12554
+ return { pass: true, name: m.name, artifactsVerified: verified, softWarnings };
12555
+ }
12556
+
12557
+ // src/pack/publish/index.ts
12558
+ var REQUIRED_SOURCE_FILES = [
12559
+ "research.yaml",
12560
+ "audits/freeze-receipt.json",
12561
+ "audits/pack-audit.json",
12562
+ "synthesis/final-report.md",
12563
+ "synthesis/decision-brief.md"
12564
+ ];
12565
+ async function publish(input) {
12566
+ const fromDir = resolve24(input.fromDir);
12567
+ const toDir = resolve24(input.toDir);
12568
+ const packageName = basename(toDir);
12569
+ const warnings = [];
12570
+ for (const rel of REQUIRED_SOURCE_FILES) {
12571
+ if (!existsSync30(join31(fromDir, rel))) {
12572
+ throw new Error(
12573
+ `Source pack missing required file: ${rel}
12574
+ Hint: run research-os freeze before publish
12575
+ Pack: ${fromDir}`
12576
+ );
12577
+ }
12578
+ }
12579
+ if (existsSync30(join31(fromDir, "audits/freeze-refusal.json")) || existsSync30(join31(fromDir, "audits/freeze-refusal.md"))) {
12580
+ throw new Error(
12581
+ `Source pack has freeze-refusal artifacts \u2014 pack did not freeze cleanly.
12582
+ Resolve blocking reasons then re-run research-os freeze.
12583
+ Pack: ${fromDir}`
12584
+ );
12585
+ }
12586
+ if (existsSync30(toDir)) {
12587
+ const entries = readdirSync2(toDir);
12588
+ if (entries.length > 0 && !input.force) {
12589
+ throw new Error(
12590
+ `Target directory already exists and is non-empty: ${toDir}
12591
+ Use --force to overwrite.`
12592
+ );
12593
+ }
12594
+ }
12595
+ const manifest = deriveManifest(fromDir, packageName, input.operatorNotes ?? "");
12596
+ if (input.dryRun) {
12597
+ const finalReportPath2 = join31(fromDir, "synthesis/final-report.md");
12598
+ const finalReport2 = existsSync30(finalReportPath2) ? readFileSync3(finalReportPath2, "utf8") : "";
12599
+ const readme2 = generateReadme(manifest, finalReport2);
12600
+ return {
12601
+ packageName,
12602
+ filesWritten: [],
12603
+ warnings: ["dry-run: no files written"],
12604
+ verifyPassed: false,
12605
+ dryRun: true,
12606
+ dryRunManifest: manifest,
12607
+ dryRunReadme: readme2
12608
+ };
12609
+ }
12610
+ mkdirSync3(toDir, { recursive: true });
12611
+ const filesWritten = [];
12612
+ const packTarget = join31(toDir, "pack");
12613
+ const packFileCount = copyDir(fromDir, packTarget);
12614
+ filesWritten.push(`pack/ (${packFileCount} files)`);
12615
+ const synthSrc = join31(fromDir, "synthesis");
12616
+ if (existsSync30(synthSrc)) {
12617
+ const synthTarget = join31(toDir, "synthesis");
12618
+ const synthFileCount = copyDir(synthSrc, synthTarget);
12619
+ filesWritten.push(`synthesis/ (${synthFileCount} files)`);
12620
+ } else {
12621
+ warnings.push(
12622
+ "No synthesis/ directory in source pack \u2014 Lane 1 synthesis files not written"
12623
+ );
12624
+ }
12625
+ writeFileSync2(
12626
+ join31(toDir, "pack.manifest.json"),
12627
+ JSON.stringify(manifest, null, 2) + "\n",
12628
+ "utf8"
12629
+ );
12630
+ filesWritten.push("pack.manifest.json");
12631
+ const finalReportPath = join31(fromDir, "synthesis/final-report.md");
12632
+ const finalReport = existsSync30(finalReportPath) ? readFileSync3(finalReportPath, "utf8") : "";
12633
+ const readme = generateReadme(manifest, finalReport);
12634
+ writeFileSync2(join31(toDir, "README.md"), readme, "utf8");
12635
+ filesWritten.push("README.md");
12636
+ const docsDir = join31(toDir, "docs");
12637
+ mkdirSync3(docsDir, { recursive: true });
12638
+ const howToReadPath = join31(docsDir, "how-to-read-this.md");
12639
+ if (existsSync30(howToReadPath)) {
12640
+ warnings.push(
12641
+ "docs/how-to-read-this.md already exists \u2014 not overwritten (operator-authored content preserved)"
12642
+ );
12643
+ } else {
12644
+ const scaffold = generateHowToReadScaffold(manifest);
12645
+ writeFileSync2(howToReadPath, scaffold, "utf8");
12646
+ filesWritten.push("docs/how-to-read-this.md");
12647
+ }
12648
+ const verifyResult = verifyPack(toDir);
12649
+ for (const w of verifyResult.softWarnings ?? []) warnings.push(w);
12650
+ if (!verifyResult.pass) {
12651
+ throw new Error(
12652
+ `Pack verification FAILED after publish \u2014 the published package does not meet the admission contract.
12653
+ ${verifyResult.reason}
12654
+ Target: ${toDir}`
12655
+ );
12656
+ }
12657
+ return {
12658
+ packageName,
12659
+ filesWritten,
12660
+ warnings,
12661
+ verifyPassed: true,
12662
+ dryRun: false
12663
+ };
12664
+ }
12665
+
12666
+ // src/cli.ts
12124
12667
  init_errors();
12125
12668
  init_src();
12126
- import { Command } from "commander";
12127
12669
  function reportError(err) {
12128
12670
  if (err instanceof ResearchOSError) {
12129
12671
  process.stderr.write(`research-os: ${err.code}: ${err.message}
@@ -12554,13 +13096,21 @@ contradictCmd.command("map").description("Detect contradiction candidates among
12554
13096
  "--triaged-only",
12555
13097
  "Only consider claims that triage selected_for_review; reduces N\xB2 pair classification on dense sections",
12556
13098
  false
13099
+ ).addOption(
13100
+ new Option(
13101
+ "--detector <mode>",
13102
+ "Detector to use: auto (default, env-var-driven), heuristic (always fast, no LLM), ollama-intern (require LLM, fail visibly if unavailable)"
13103
+ ).choices(["auto", "heuristic", "ollama-intern"]).default("auto")
12557
13104
  ).action(async (section, opts) => {
12558
13105
  try {
12559
13106
  const result = await map({
12560
13107
  sectionId: section,
12561
13108
  packPath: opts.pack,
12562
- triagedOnly: opts.triagedOnly
13109
+ triagedOnly: opts.triagedOnly,
13110
+ detectorMode: opts.detector
12563
13111
  });
13112
+ process.stdout.write(`${result.detectorAnnouncement}
13113
+ `);
12564
13114
  process.stdout.write(`contradiction map complete
12565
13115
  `);
12566
13116
  process.stdout.write(` section: ${result.sectionId}
@@ -13181,5 +13731,59 @@ program.command("review-promote").description(
13181
13731
  reportError(err);
13182
13732
  }
13183
13733
  });
13734
+ var packCmd = program.command("pack").description("Pack-level publication and archive operations");
13735
+ packCmd.command("publish").description(
13736
+ "Export a frozen pack into the research-packs archive format. Copies the pack, derives pack.manifest.json, generates README.md, provisions docs/how-to-read-this.md, and verifies the admission contract."
13737
+ ).requiredOption("--to <path>", "Target package directory, e.g. <research-packs>/packages/<name>").option("--from <path>", "Source frozen pack directory (defaults to cwd)", process.cwd()).option("--operator-notes <text>", "Operator notes recorded in pack.manifest.json", "").option("--force", "Overwrite an existing non-empty target directory", false).option("--dry-run", "Print derived manifest and README plan; write nothing", false).action(async (opts) => {
13738
+ try {
13739
+ const result = await publish({
13740
+ fromDir: opts.from,
13741
+ toDir: opts.to,
13742
+ operatorNotes: opts.operatorNotes,
13743
+ force: Boolean(opts.force),
13744
+ dryRun: Boolean(opts.dryRun)
13745
+ });
13746
+ if (result.dryRun) {
13747
+ process.stdout.write(`pack publish: DRY-RUN \u2014 no files written
13748
+ `);
13749
+ process.stdout.write(` package name: ${result.packageName}
13750
+ `);
13751
+ if (result.dryRunManifest) {
13752
+ const m = result.dryRunManifest;
13753
+ process.stdout.write(` topic: ${m.topic.slice(0, 80)}
13754
+ `);
13755
+ process.stdout.write(` frozen_at: ${m.frozen_at}
13756
+ `);
13757
+ process.stdout.write(` sections: ${m.totals.sections}
13758
+ `);
13759
+ process.stdout.write(` accepted: ${m.totals.accepted_claims}
13760
+ `);
13761
+ process.stdout.write(` receipt sha256:${m.freeze_receipt_sha256.slice(0, 16)}\u2026
13762
+ `);
13763
+ }
13764
+ return;
13765
+ }
13766
+ process.stdout.write(`pack publish: DONE
13767
+ `);
13768
+ process.stdout.write(` package name: ${result.packageName}
13769
+ `);
13770
+ process.stdout.write(` files written: ${result.filesWritten.length}
13771
+ `);
13772
+ for (const f of result.filesWritten) process.stdout.write(` ${f}
13773
+ `);
13774
+ process.stdout.write(` verify: ${result.verifyPassed ? "PASS" : "FAIL"}
13775
+ `);
13776
+ if (result.warnings.length > 0) {
13777
+ process.stdout.write(`
13778
+ warnings:
13779
+ `);
13780
+ for (const w of result.warnings) process.stdout.write(` - ${w}
13781
+ `);
13782
+ }
13783
+ if (!result.verifyPassed) process.exitCode = 2;
13784
+ } catch (err) {
13785
+ reportError(err);
13786
+ }
13787
+ });
13184
13788
  program.parseAsync(process.argv);
13185
13789
  //# sourceMappingURL=cli.js.map