@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/CHANGELOG.md +152 -0
- package/README.md +80 -60
- package/dist/cli.js +616 -12
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.js +54 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|