@mcptoolshop/research-os 0.1.1 → 0.2.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 +74 -0
- package/README.md +78 -60
- package/dist/cli.js +558 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7265,6 +7265,7 @@ function buildSectionState(args) {
|
|
|
7265
7265
|
candidate_claims_total: candidateClaims.length,
|
|
7266
7266
|
unresolved_contradiction_ids: unresolved.map((c) => c.contradiction_id),
|
|
7267
7267
|
blocking_reasons: gate2?.blocking_reasons ?? [],
|
|
7268
|
+
active_blockers: gate2?.blocking_reasons ?? [],
|
|
7268
7269
|
blocking_contradictions_unresolved: blocking.length,
|
|
7269
7270
|
provenance_summary: provenanceSummary
|
|
7270
7271
|
};
|
|
@@ -7308,7 +7309,7 @@ function determineMode(sections, waivers, warnings) {
|
|
|
7308
7309
|
return "human_review_required";
|
|
7309
7310
|
}
|
|
7310
7311
|
const allReady = sections.every(
|
|
7311
|
-
(s) => s.has_gate_run && s.synthesis_eligible && s.has_review_run && s.candidate_claims_total > 0 && s.
|
|
7312
|
+
(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
7313
|
);
|
|
7313
7314
|
if (allReady && sections.length > 0) return "synthesis_ready";
|
|
7314
7315
|
return "repair_required";
|
|
@@ -7517,6 +7518,7 @@ var init_schema13 = __esm({
|
|
|
7517
7518
|
candidate_claims_total: z14.number().int().nonnegative(),
|
|
7518
7519
|
unresolved_contradiction_ids: z14.array(z14.string()),
|
|
7519
7520
|
blocking_reasons: z14.array(z14.string()),
|
|
7521
|
+
active_blockers: z14.array(z14.string()).default([]),
|
|
7520
7522
|
blocking_contradictions_unresolved: z14.number().int().nonnegative(),
|
|
7521
7523
|
provenance_summary: ProvenanceSummarySchema.optional()
|
|
7522
7524
|
});
|
|
@@ -9133,7 +9135,7 @@ function buildReadinessSummary(rows, handoff2, unresolvedContradictions) {
|
|
|
9133
9135
|
if (!r.has_gate_run) noGate += 1;
|
|
9134
9136
|
if (!r.has_review_run) noReview += 1;
|
|
9135
9137
|
const unresolvedCount = unresolvedBySection.get(r.section_id) ?? 0;
|
|
9136
|
-
if (r.synthesis_eligible && r.has_review_run && r.candidate_claims > 0 && r.
|
|
9138
|
+
if (r.synthesis_eligible && r.has_review_run && r.candidate_claims > 0 && r.blocking_reasons.length === 0 && unresolvedCount === 0) {
|
|
9137
9139
|
ready += 1;
|
|
9138
9140
|
} else if (r.has_gate_run && r.gate_verdict === "blocked" && !r.synthesis_eligible) {
|
|
9139
9141
|
blocked += 1;
|
|
@@ -11690,7 +11692,7 @@ var init_src = __esm({
|
|
|
11690
11692
|
init_triage();
|
|
11691
11693
|
init_discover();
|
|
11692
11694
|
init_errors();
|
|
11693
|
-
RESEARCH_OS_VERSION = "0.
|
|
11695
|
+
RESEARCH_OS_VERSION = "0.2.0";
|
|
11694
11696
|
}
|
|
11695
11697
|
});
|
|
11696
11698
|
|
|
@@ -12121,9 +12123,507 @@ init_synth();
|
|
|
12121
12123
|
init_audit();
|
|
12122
12124
|
init_freeze();
|
|
12123
12125
|
init_invalidate();
|
|
12126
|
+
import { Command } from "commander";
|
|
12127
|
+
|
|
12128
|
+
// src/pack/publish/index.ts
|
|
12129
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
12130
|
+
import { join as join31, basename, resolve as resolve24 } from "path";
|
|
12131
|
+
|
|
12132
|
+
// src/pack/publish/manifest.ts
|
|
12133
|
+
init_schema();
|
|
12134
|
+
import { createHash as createHash11 } from "crypto";
|
|
12135
|
+
import { readFileSync, existsSync as existsSync28 } from "fs";
|
|
12136
|
+
import { join as join28 } from "path";
|
|
12137
|
+
import { parse as parseYaml } from "yaml";
|
|
12138
|
+
import { z as z23 } from "zod";
|
|
12139
|
+
|
|
12140
|
+
// src/pack/publish/schema.ts
|
|
12141
|
+
import { z as z22 } from "zod";
|
|
12142
|
+
var SectionSummarySchema = z22.object({
|
|
12143
|
+
id: z22.string().min(1),
|
|
12144
|
+
accepted_claims: z22.number().int().min(0),
|
|
12145
|
+
gate: z22.enum(["pass", "warn", "fail", "blocked", "pass_with_waiver"]),
|
|
12146
|
+
synthesis_eligible: z22.boolean()
|
|
12147
|
+
});
|
|
12148
|
+
var TotalsSchema = z22.object({
|
|
12149
|
+
sections: z22.number().int().min(1),
|
|
12150
|
+
accepted_claims: z22.number().int().min(0),
|
|
12151
|
+
dispositioned: z22.number().int().min(0),
|
|
12152
|
+
unresolved_contradictions: z22.number().int().min(0),
|
|
12153
|
+
preserved_contradiction_records: z22.number().int().min(0).optional()
|
|
12154
|
+
});
|
|
12155
|
+
var PackManifestSchema = z22.object({
|
|
12156
|
+
name: z22.string().min(1),
|
|
12157
|
+
topic: z22.string().min(1),
|
|
12158
|
+
frozen_at: z22.string().datetime(),
|
|
12159
|
+
research_os_version: z22.string().min(1),
|
|
12160
|
+
sections: z22.array(SectionSummarySchema).min(1),
|
|
12161
|
+
totals: TotalsSchema,
|
|
12162
|
+
freeze_receipt_sha256: z22.string().regex(/^[a-f0-9]{64}$/, "Must be a 64-char hex sha256"),
|
|
12163
|
+
operator_notes: z22.string().default("")
|
|
12164
|
+
});
|
|
12165
|
+
|
|
12166
|
+
// src/pack/publish/manifest.ts
|
|
12167
|
+
var GateResultMinimalSchema = z23.object({
|
|
12168
|
+
verdict: z23.enum(["pass", "warn", "fail", "blocked"]),
|
|
12169
|
+
synthesis_eligible: z23.boolean()
|
|
12170
|
+
});
|
|
12171
|
+
function sha256Bytes(buf) {
|
|
12172
|
+
return createHash11("sha256").update(buf).digest("hex");
|
|
12173
|
+
}
|
|
12174
|
+
function parseJsonl(content) {
|
|
12175
|
+
return content.split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
|
|
12176
|
+
}
|
|
12177
|
+
function readJsonlSafe(filePath) {
|
|
12178
|
+
if (!existsSync28(filePath)) return [];
|
|
12179
|
+
return parseJsonl(readFileSync(filePath, "utf8"));
|
|
12180
|
+
}
|
|
12181
|
+
function latestClaimDecisions(reviews) {
|
|
12182
|
+
const m = /* @__PURE__ */ new Map();
|
|
12183
|
+
const sorted = [...reviews].sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
12184
|
+
for (const r of sorted) m.set(r.claim_id, r.decision);
|
|
12185
|
+
return m;
|
|
12186
|
+
}
|
|
12187
|
+
function latestContradictionStatuses(resolutions) {
|
|
12188
|
+
const m = /* @__PURE__ */ new Map();
|
|
12189
|
+
const sorted = [...resolutions].sort((a, b) => a.resolved_at.localeCompare(b.resolved_at));
|
|
12190
|
+
for (const r of sorted) m.set(r.contradiction_id, r.status);
|
|
12191
|
+
return m;
|
|
12192
|
+
}
|
|
12193
|
+
function latestDispositionStatuses(dispositions) {
|
|
12194
|
+
const m = /* @__PURE__ */ new Map();
|
|
12195
|
+
const sorted = [...dispositions].sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
12196
|
+
for (const d of sorted) m.set(d.claim_id, "dispositioned");
|
|
12197
|
+
return m;
|
|
12198
|
+
}
|
|
12199
|
+
function deriveManifest(packDir, packageName, operatorNotes = "") {
|
|
12200
|
+
const yamlPath = join28(packDir, "research.yaml");
|
|
12201
|
+
if (!existsSync28(yamlPath)) throw new Error(`research.yaml not found in ${packDir}`);
|
|
12202
|
+
const research = ResearchYamlSchema.parse(parseYaml(readFileSync(yamlPath, "utf8")));
|
|
12203
|
+
if (!research.frozen_at) {
|
|
12204
|
+
throw new Error(`Pack is not frozen: research.yaml.frozen_at is null \u2014 run research-os freeze first`);
|
|
12205
|
+
}
|
|
12206
|
+
const receiptPath = join28(packDir, "audits/freeze-receipt.json");
|
|
12207
|
+
if (!existsSync28(receiptPath)) {
|
|
12208
|
+
throw new Error(`audits/freeze-receipt.json not found \u2014 pack is not frozen`);
|
|
12209
|
+
}
|
|
12210
|
+
const receiptBytes = readFileSync(receiptPath);
|
|
12211
|
+
const freeze_receipt_sha256 = sha256Bytes(receiptBytes);
|
|
12212
|
+
const receipt = JSON.parse(receiptBytes.toString("utf8"));
|
|
12213
|
+
const frozen_at = receipt.frozen_at ?? research.frozen_at;
|
|
12214
|
+
const packAuditPath = join28(packDir, "audits/pack-audit.json");
|
|
12215
|
+
if (!existsSync28(packAuditPath)) throw new Error(`audits/pack-audit.json not found`);
|
|
12216
|
+
const packAudit = JSON.parse(readFileSync(packAuditPath, "utf8"));
|
|
12217
|
+
const auditSectionMap = new Map(
|
|
12218
|
+
(packAudit.section_summaries ?? []).map((s) => [s.section_id, s.accepted_claims])
|
|
12219
|
+
);
|
|
12220
|
+
const sectionIds = research.sections.map((s) => s.id);
|
|
12221
|
+
const sections = [];
|
|
12222
|
+
let totalAccepted = 0;
|
|
12223
|
+
let totalDispositioned = 0;
|
|
12224
|
+
let totalPreserved = 0;
|
|
12225
|
+
for (const sectionId of sectionIds) {
|
|
12226
|
+
const sectionDir = join28(packDir, "sections", sectionId);
|
|
12227
|
+
const reviews = readJsonlSafe(join28(sectionDir, "claim-reviews.jsonl"));
|
|
12228
|
+
const decisionMap = latestClaimDecisions(reviews);
|
|
12229
|
+
const acceptedCount = [...decisionMap.values()].filter(
|
|
12230
|
+
(d) => d === "accepted_for_synthesis"
|
|
12231
|
+
).length;
|
|
12232
|
+
const auditAccepted = auditSectionMap.get(sectionId);
|
|
12233
|
+
if (auditAccepted !== void 0 && auditAccepted !== acceptedCount) {
|
|
12234
|
+
throw new Error(
|
|
12235
|
+
`Section ${sectionId}: accepted_claims mismatch between claim-reviews.jsonl (${acceptedCount}) and pack-audit.json (${auditAccepted}). Closure-ledger seam disagreement \u2014 investigate before publishing.`
|
|
12236
|
+
);
|
|
12237
|
+
}
|
|
12238
|
+
const gateResultPath = join28(packDir, "audits", `${sectionId}-gate.json`);
|
|
12239
|
+
if (!existsSync28(gateResultPath)) {
|
|
12240
|
+
throw new Error(`audits/${sectionId}-gate.json not found \u2014 section not gated`);
|
|
12241
|
+
}
|
|
12242
|
+
const gateResult = GateResultMinimalSchema.parse(
|
|
12243
|
+
JSON.parse(readFileSync(gateResultPath, "utf8"))
|
|
12244
|
+
);
|
|
12245
|
+
const dispositions = readJsonlSafe(
|
|
12246
|
+
join28(sectionDir, "claim-synthesis-dispositions.jsonl")
|
|
12247
|
+
);
|
|
12248
|
+
const dispositionMap = latestDispositionStatuses(dispositions);
|
|
12249
|
+
totalDispositioned += dispositionMap.size;
|
|
12250
|
+
const resolutions = readJsonlSafe(
|
|
12251
|
+
join28(sectionDir, "contradiction-resolutions.jsonl")
|
|
12252
|
+
);
|
|
12253
|
+
const resolutionMap = latestContradictionStatuses(resolutions);
|
|
12254
|
+
const stillUnresolved = [...resolutionMap.values()].filter((s) => s === "unresolved").length;
|
|
12255
|
+
if (stillUnresolved > 0) {
|
|
12256
|
+
throw new Error(
|
|
12257
|
+
`Section ${sectionId} has ${stillUnresolved} unresolved contradictions. Freeze should have blocked this \u2014 investigate before publishing.`
|
|
12258
|
+
);
|
|
12259
|
+
}
|
|
12260
|
+
totalPreserved += [...resolutionMap.values()].filter((s) => s !== "unresolved").length;
|
|
12261
|
+
totalAccepted += acceptedCount;
|
|
12262
|
+
sections.push({
|
|
12263
|
+
id: sectionId,
|
|
12264
|
+
accepted_claims: acceptedCount,
|
|
12265
|
+
gate: gateResult.verdict,
|
|
12266
|
+
synthesis_eligible: gateResult.synthesis_eligible
|
|
12267
|
+
});
|
|
12268
|
+
}
|
|
12269
|
+
const totalsBase = {
|
|
12270
|
+
sections: sections.length,
|
|
12271
|
+
accepted_claims: totalAccepted,
|
|
12272
|
+
dispositioned: totalDispositioned,
|
|
12273
|
+
unresolved_contradictions: 0
|
|
12274
|
+
};
|
|
12275
|
+
return PackManifestSchema.parse({
|
|
12276
|
+
name: packageName,
|
|
12277
|
+
topic: research.topic,
|
|
12278
|
+
frozen_at,
|
|
12279
|
+
research_os_version: research.research_os_version,
|
|
12280
|
+
sections,
|
|
12281
|
+
totals: totalPreserved > 0 ? { ...totalsBase, preserved_contradiction_records: totalPreserved } : totalsBase,
|
|
12282
|
+
freeze_receipt_sha256,
|
|
12283
|
+
operator_notes: operatorNotes
|
|
12284
|
+
});
|
|
12285
|
+
}
|
|
12286
|
+
|
|
12287
|
+
// src/pack/publish/readme.ts
|
|
12288
|
+
function extractSummary(markdown) {
|
|
12289
|
+
const lines = markdown.split("\n");
|
|
12290
|
+
let inSummary = false;
|
|
12291
|
+
const summaryLines = [];
|
|
12292
|
+
for (const line of lines) {
|
|
12293
|
+
if (/^## Summary\s*$/.test(line)) {
|
|
12294
|
+
inSummary = true;
|
|
12295
|
+
continue;
|
|
12296
|
+
}
|
|
12297
|
+
if (inSummary && /^## /.test(line)) break;
|
|
12298
|
+
if (inSummary) summaryLines.push(line);
|
|
12299
|
+
}
|
|
12300
|
+
return summaryLines.join("\n").trim();
|
|
12301
|
+
}
|
|
12302
|
+
function generateReadme(manifest, finalReport) {
|
|
12303
|
+
const m = manifest;
|
|
12304
|
+
const frozenDate = m.frozen_at.slice(0, 10);
|
|
12305
|
+
const summary = extractSummary(finalReport);
|
|
12306
|
+
const sectionTable = m.sections.map((s) => `| ${s.id} | ${s.accepted_claims} | ${s.gate} | ${s.synthesis_eligible ? "yes" : "no"} |`).join("\n");
|
|
12307
|
+
const totalsLine = m.totals.preserved_contradiction_records != null ? `Preserved contradiction records: ${m.totals.preserved_contradiction_records}` : `${m.totals.unresolved_contradictions} unresolved contradictions`;
|
|
12308
|
+
const operatorSection = m.operator_notes ? `
|
|
12309
|
+
---
|
|
12310
|
+
|
|
12311
|
+
## Operator notes
|
|
12312
|
+
|
|
12313
|
+
${m.operator_notes}
|
|
12314
|
+
` : "";
|
|
12315
|
+
return `# ${m.name}
|
|
12316
|
+
|
|
12317
|
+
**Topic:** ${m.topic}
|
|
12318
|
+
|
|
12319
|
+
**Frozen:** ${frozenDate} | **research-os version:** ${m.research_os_version} | **Accepted claims:** ${m.totals.accepted_claims} across ${m.totals.sections} sections
|
|
12320
|
+
|
|
12321
|
+
---
|
|
12322
|
+
|
|
12323
|
+
## Executive summary
|
|
12324
|
+
|
|
12325
|
+
${summary}
|
|
12326
|
+
|
|
12327
|
+
---
|
|
12328
|
+
|
|
12329
|
+
## Sections
|
|
12330
|
+
|
|
12331
|
+
| Section | Accepted claims | Gate | Synthesis eligible |
|
|
12332
|
+
|---------|-----------------|------|-------------------|
|
|
12333
|
+
${sectionTable}
|
|
12334
|
+
|
|
12335
|
+
**Totals:** ${m.totals.accepted_claims} accepted, ${m.totals.dispositioned} dispositioned, ${totalsLine}
|
|
12336
|
+
|
|
12337
|
+
---
|
|
12338
|
+
|
|
12339
|
+
## How to read this pack
|
|
12340
|
+
|
|
12341
|
+
This package is part of the [\`research-packs\`](../../README.md) archive.
|
|
12342
|
+
|
|
12343
|
+
- **Lane 1 (synthesis):** You are here. See [\`synthesis/final-report.md\`](synthesis/final-report.md) for the full citation-clean prose.
|
|
12344
|
+
- **Lane 2 (evidence):** [\`pack/\`](pack/) \u2014 full frozen ledgers, source cards, excerpts, claim reviews, gate results, and \`audits/freeze-receipt.json\`.
|
|
12345
|
+
- **Lane 3 (method):** [\`../../docs/\`](../../docs/) \u2014 artifact contract, how-to-read, source quality notes.
|
|
12346
|
+
|
|
12347
|
+
To verify this pack's integrity: \`node ../../scripts/verify-pack.mjs .\` from this directory.
|
|
12348
|
+
|
|
12349
|
+
See [\`docs/how-to-read-this.md\`](docs/how-to-read-this.md) for pack-specific reading notes.${operatorSection}`;
|
|
12350
|
+
}
|
|
12351
|
+
|
|
12352
|
+
// src/pack/publish/how-to-read.ts
|
|
12353
|
+
function generateHowToReadScaffold(manifest) {
|
|
12354
|
+
const frozenDate = manifest.frozen_at.slice(0, 10);
|
|
12355
|
+
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.";
|
|
12356
|
+
return `# How to read: ${manifest.name}
|
|
12357
|
+
|
|
12358
|
+
<!-- SCAFFOLD: Pre-filled with pack-specific metadata by \`research-os pack publish\`.
|
|
12359
|
+
Final prose is human-authored. Edit freely \u2014 this file is NOT covered by the freeze receipt. -->
|
|
12360
|
+
|
|
12361
|
+
**Pack:** \`${manifest.name}\`
|
|
12362
|
+
**Topic:** ${manifest.topic}
|
|
12363
|
+
**Frozen:** ${frozenDate}
|
|
12364
|
+
**Accepted claims:** ${manifest.totals.accepted_claims} accepted claims across ${manifest.totals.sections} section${manifest.totals.sections !== 1 ? "s" : ""}
|
|
12365
|
+
|
|
12366
|
+
---
|
|
12367
|
+
|
|
12368
|
+
## What this pack answers
|
|
12369
|
+
|
|
12370
|
+
<!-- human-authored: describe what the synthesis delivers -->
|
|
12371
|
+
|
|
12372
|
+
## How the evidence is structured
|
|
12373
|
+
|
|
12374
|
+
<!-- human-authored: explain section breakdown, what each section investigated -->
|
|
12375
|
+
|
|
12376
|
+
## Interpreting claim IDs
|
|
12377
|
+
|
|
12378
|
+
Claims are referenced as \`[claim:clm_<hex>]\` in the synthesis prose. To look up a claim:
|
|
12379
|
+
|
|
12380
|
+
1. Open \`pack/sections/<section-id>/claims.jsonl\`
|
|
12381
|
+
2. Find the entry with matching \`claim_id\`
|
|
12382
|
+
3. The \`asserts\` field is the claim; \`evidence_excerpt\` is the literal source span
|
|
12383
|
+
|
|
12384
|
+
Accepted vs rejected: \`pack/sections/<section-id>/claim-reviews.jsonl\` \u2014 latest entry per \`claim_id\` wins.
|
|
12385
|
+
|
|
12386
|
+
## Preserved contradiction records
|
|
12387
|
+
|
|
12388
|
+
${contradictionNote}
|
|
12389
|
+
|
|
12390
|
+
## Verifying integrity
|
|
12391
|
+
|
|
12392
|
+
From this directory:
|
|
12393
|
+
|
|
12394
|
+
\`\`\`bash
|
|
12395
|
+
node ../../scripts/verify-pack.mjs .
|
|
12396
|
+
\`\`\`
|
|
12397
|
+
|
|
12398
|
+
Expected output: PASS with artifact count and receipt sha256.
|
|
12399
|
+
|
|
12400
|
+
See [\`../../docs/how-to-read-a-pack.md\`](../../docs/how-to-read-a-pack.md) for the general guide.
|
|
12401
|
+
`;
|
|
12402
|
+
}
|
|
12403
|
+
|
|
12404
|
+
// src/pack/publish/copy.ts
|
|
12405
|
+
import { mkdirSync as mkdirSync2, copyFileSync, readdirSync } from "fs";
|
|
12406
|
+
import { join as join29 } from "path";
|
|
12407
|
+
function copyDir(src, dst) {
|
|
12408
|
+
mkdirSync2(dst, { recursive: true });
|
|
12409
|
+
let count = 0;
|
|
12410
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
12411
|
+
for (const entry of entries) {
|
|
12412
|
+
const srcPath = join29(src, entry.name);
|
|
12413
|
+
const dstPath = join29(dst, entry.name);
|
|
12414
|
+
if (entry.isDirectory()) {
|
|
12415
|
+
count += copyDir(srcPath, dstPath);
|
|
12416
|
+
} else if (entry.isFile()) {
|
|
12417
|
+
copyFileSync(srcPath, dstPath);
|
|
12418
|
+
count++;
|
|
12419
|
+
}
|
|
12420
|
+
}
|
|
12421
|
+
return count;
|
|
12422
|
+
}
|
|
12423
|
+
|
|
12424
|
+
// src/pack/publish/verify.ts
|
|
12425
|
+
import { createHash as createHash12 } from "crypto";
|
|
12426
|
+
import { readFileSync as readFileSync2, existsSync as existsSync29 } from "fs";
|
|
12427
|
+
import { join as join30 } from "path";
|
|
12428
|
+
var REQUIRED_FILES = [
|
|
12429
|
+
"pack/audits/freeze-receipt.json",
|
|
12430
|
+
"synthesis/final-report.md",
|
|
12431
|
+
"synthesis/decision-brief.md",
|
|
12432
|
+
"pack.manifest.json",
|
|
12433
|
+
"README.md"
|
|
12434
|
+
];
|
|
12435
|
+
function sha256File(filePath) {
|
|
12436
|
+
return createHash12("sha256").update(readFileSync2(filePath)).digest("hex");
|
|
12437
|
+
}
|
|
12438
|
+
function verifyPack(packageDir) {
|
|
12439
|
+
for (const rel of REQUIRED_FILES) {
|
|
12440
|
+
const full = join30(packageDir, rel);
|
|
12441
|
+
if (!existsSync29(full)) {
|
|
12442
|
+
return { pass: false, reason: `MISSING required file: ${rel}` };
|
|
12443
|
+
}
|
|
12444
|
+
}
|
|
12445
|
+
let rawManifest;
|
|
12446
|
+
try {
|
|
12447
|
+
rawManifest = JSON.parse(readFileSync2(join30(packageDir, "pack.manifest.json"), "utf8"));
|
|
12448
|
+
} catch (e) {
|
|
12449
|
+
return { pass: false, reason: `pack.manifest.json parse error: ${e.message}` };
|
|
12450
|
+
}
|
|
12451
|
+
const parsed = PackManifestSchema.safeParse(rawManifest);
|
|
12452
|
+
if (!parsed.success) {
|
|
12453
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
|
|
12454
|
+
return { pass: false, reason: `pack.manifest.json schema violation: ${issues}` };
|
|
12455
|
+
}
|
|
12456
|
+
const m = parsed.data;
|
|
12457
|
+
const receiptPath = join30(packageDir, "pack/audits/freeze-receipt.json");
|
|
12458
|
+
const actualReceiptHash = sha256File(receiptPath);
|
|
12459
|
+
if (actualReceiptHash !== m.freeze_receipt_sha256) {
|
|
12460
|
+
return {
|
|
12461
|
+
pass: false,
|
|
12462
|
+
reason: `freeze-receipt.json hash mismatch.
|
|
12463
|
+
manifest: ${m.freeze_receipt_sha256}
|
|
12464
|
+
actual: ${actualReceiptHash}`,
|
|
12465
|
+
name: m.name
|
|
12466
|
+
};
|
|
12467
|
+
}
|
|
12468
|
+
let receipt;
|
|
12469
|
+
try {
|
|
12470
|
+
receipt = JSON.parse(readFileSync2(receiptPath, "utf8"));
|
|
12471
|
+
} catch (e) {
|
|
12472
|
+
return {
|
|
12473
|
+
pass: false,
|
|
12474
|
+
reason: `freeze-receipt.json parse error: ${e.message}`,
|
|
12475
|
+
name: m.name
|
|
12476
|
+
};
|
|
12477
|
+
}
|
|
12478
|
+
const allFingerprints = [
|
|
12479
|
+
...receipt.canonical_artifact_hashes ?? [],
|
|
12480
|
+
...receipt.synthesis_hashes ?? []
|
|
12481
|
+
];
|
|
12482
|
+
let verified = 0;
|
|
12483
|
+
const softWarnings = [];
|
|
12484
|
+
for (const entry of allFingerprints) {
|
|
12485
|
+
const artifactPath = join30(packageDir, "pack", entry.path);
|
|
12486
|
+
if (!existsSync29(artifactPath)) {
|
|
12487
|
+
return {
|
|
12488
|
+
pass: false,
|
|
12489
|
+
reason: `Fingerprinted artifact missing: pack/${entry.path}`,
|
|
12490
|
+
name: m.name
|
|
12491
|
+
};
|
|
12492
|
+
}
|
|
12493
|
+
const actualHash = sha256File(artifactPath);
|
|
12494
|
+
if (actualHash !== entry.sha256) {
|
|
12495
|
+
if (entry.path === "research.yaml") {
|
|
12496
|
+
softWarnings.push(
|
|
12497
|
+
`WARN pack/research.yaml hash reflects pre-freeze state (known: freeze writes frozen_at + status after fingerprinting)`
|
|
12498
|
+
);
|
|
12499
|
+
verified++;
|
|
12500
|
+
continue;
|
|
12501
|
+
}
|
|
12502
|
+
return {
|
|
12503
|
+
pass: false,
|
|
12504
|
+
reason: `Hash mismatch for pack/${entry.path}.
|
|
12505
|
+
receipt: ${entry.sha256}
|
|
12506
|
+
actual: ${actualHash}`,
|
|
12507
|
+
name: m.name
|
|
12508
|
+
};
|
|
12509
|
+
}
|
|
12510
|
+
verified++;
|
|
12511
|
+
}
|
|
12512
|
+
return { pass: true, name: m.name, artifactsVerified: verified, softWarnings };
|
|
12513
|
+
}
|
|
12514
|
+
|
|
12515
|
+
// src/pack/publish/index.ts
|
|
12516
|
+
var REQUIRED_SOURCE_FILES = [
|
|
12517
|
+
"research.yaml",
|
|
12518
|
+
"audits/freeze-receipt.json",
|
|
12519
|
+
"audits/pack-audit.json",
|
|
12520
|
+
"synthesis/final-report.md",
|
|
12521
|
+
"synthesis/decision-brief.md"
|
|
12522
|
+
];
|
|
12523
|
+
async function publish(input) {
|
|
12524
|
+
const fromDir = resolve24(input.fromDir);
|
|
12525
|
+
const toDir = resolve24(input.toDir);
|
|
12526
|
+
const packageName = basename(toDir);
|
|
12527
|
+
const warnings = [];
|
|
12528
|
+
for (const rel of REQUIRED_SOURCE_FILES) {
|
|
12529
|
+
if (!existsSync30(join31(fromDir, rel))) {
|
|
12530
|
+
throw new Error(
|
|
12531
|
+
`Source pack missing required file: ${rel}
|
|
12532
|
+
Hint: run research-os freeze before publish
|
|
12533
|
+
Pack: ${fromDir}`
|
|
12534
|
+
);
|
|
12535
|
+
}
|
|
12536
|
+
}
|
|
12537
|
+
if (existsSync30(join31(fromDir, "audits/freeze-refusal.json")) || existsSync30(join31(fromDir, "audits/freeze-refusal.md"))) {
|
|
12538
|
+
throw new Error(
|
|
12539
|
+
`Source pack has freeze-refusal artifacts \u2014 pack did not freeze cleanly.
|
|
12540
|
+
Resolve blocking reasons then re-run research-os freeze.
|
|
12541
|
+
Pack: ${fromDir}`
|
|
12542
|
+
);
|
|
12543
|
+
}
|
|
12544
|
+
if (existsSync30(toDir)) {
|
|
12545
|
+
const entries = readdirSync2(toDir);
|
|
12546
|
+
if (entries.length > 0 && !input.force) {
|
|
12547
|
+
throw new Error(
|
|
12548
|
+
`Target directory already exists and is non-empty: ${toDir}
|
|
12549
|
+
Use --force to overwrite.`
|
|
12550
|
+
);
|
|
12551
|
+
}
|
|
12552
|
+
}
|
|
12553
|
+
const manifest = deriveManifest(fromDir, packageName, input.operatorNotes ?? "");
|
|
12554
|
+
if (input.dryRun) {
|
|
12555
|
+
const finalReportPath2 = join31(fromDir, "synthesis/final-report.md");
|
|
12556
|
+
const finalReport2 = existsSync30(finalReportPath2) ? readFileSync3(finalReportPath2, "utf8") : "";
|
|
12557
|
+
const readme2 = generateReadme(manifest, finalReport2);
|
|
12558
|
+
return {
|
|
12559
|
+
packageName,
|
|
12560
|
+
filesWritten: [],
|
|
12561
|
+
warnings: ["dry-run: no files written"],
|
|
12562
|
+
verifyPassed: false,
|
|
12563
|
+
dryRun: true,
|
|
12564
|
+
dryRunManifest: manifest,
|
|
12565
|
+
dryRunReadme: readme2
|
|
12566
|
+
};
|
|
12567
|
+
}
|
|
12568
|
+
mkdirSync3(toDir, { recursive: true });
|
|
12569
|
+
const filesWritten = [];
|
|
12570
|
+
const packTarget = join31(toDir, "pack");
|
|
12571
|
+
const packFileCount = copyDir(fromDir, packTarget);
|
|
12572
|
+
filesWritten.push(`pack/ (${packFileCount} files)`);
|
|
12573
|
+
const synthSrc = join31(fromDir, "synthesis");
|
|
12574
|
+
if (existsSync30(synthSrc)) {
|
|
12575
|
+
const synthTarget = join31(toDir, "synthesis");
|
|
12576
|
+
const synthFileCount = copyDir(synthSrc, synthTarget);
|
|
12577
|
+
filesWritten.push(`synthesis/ (${synthFileCount} files)`);
|
|
12578
|
+
} else {
|
|
12579
|
+
warnings.push(
|
|
12580
|
+
"No synthesis/ directory in source pack \u2014 Lane 1 synthesis files not written"
|
|
12581
|
+
);
|
|
12582
|
+
}
|
|
12583
|
+
writeFileSync2(
|
|
12584
|
+
join31(toDir, "pack.manifest.json"),
|
|
12585
|
+
JSON.stringify(manifest, null, 2) + "\n",
|
|
12586
|
+
"utf8"
|
|
12587
|
+
);
|
|
12588
|
+
filesWritten.push("pack.manifest.json");
|
|
12589
|
+
const finalReportPath = join31(fromDir, "synthesis/final-report.md");
|
|
12590
|
+
const finalReport = existsSync30(finalReportPath) ? readFileSync3(finalReportPath, "utf8") : "";
|
|
12591
|
+
const readme = generateReadme(manifest, finalReport);
|
|
12592
|
+
writeFileSync2(join31(toDir, "README.md"), readme, "utf8");
|
|
12593
|
+
filesWritten.push("README.md");
|
|
12594
|
+
const docsDir = join31(toDir, "docs");
|
|
12595
|
+
mkdirSync3(docsDir, { recursive: true });
|
|
12596
|
+
const howToReadPath = join31(docsDir, "how-to-read-this.md");
|
|
12597
|
+
if (existsSync30(howToReadPath)) {
|
|
12598
|
+
warnings.push(
|
|
12599
|
+
"docs/how-to-read-this.md already exists \u2014 not overwritten (operator-authored content preserved)"
|
|
12600
|
+
);
|
|
12601
|
+
} else {
|
|
12602
|
+
const scaffold = generateHowToReadScaffold(manifest);
|
|
12603
|
+
writeFileSync2(howToReadPath, scaffold, "utf8");
|
|
12604
|
+
filesWritten.push("docs/how-to-read-this.md");
|
|
12605
|
+
}
|
|
12606
|
+
const verifyResult = verifyPack(toDir);
|
|
12607
|
+
for (const w of verifyResult.softWarnings ?? []) warnings.push(w);
|
|
12608
|
+
if (!verifyResult.pass) {
|
|
12609
|
+
throw new Error(
|
|
12610
|
+
`Pack verification FAILED after publish \u2014 the published package does not meet the admission contract.
|
|
12611
|
+
${verifyResult.reason}
|
|
12612
|
+
Target: ${toDir}`
|
|
12613
|
+
);
|
|
12614
|
+
}
|
|
12615
|
+
return {
|
|
12616
|
+
packageName,
|
|
12617
|
+
filesWritten,
|
|
12618
|
+
warnings,
|
|
12619
|
+
verifyPassed: true,
|
|
12620
|
+
dryRun: false
|
|
12621
|
+
};
|
|
12622
|
+
}
|
|
12623
|
+
|
|
12624
|
+
// src/cli.ts
|
|
12124
12625
|
init_errors();
|
|
12125
12626
|
init_src();
|
|
12126
|
-
import { Command } from "commander";
|
|
12127
12627
|
function reportError(err) {
|
|
12128
12628
|
if (err instanceof ResearchOSError) {
|
|
12129
12629
|
process.stderr.write(`research-os: ${err.code}: ${err.message}
|
|
@@ -13181,5 +13681,59 @@ program.command("review-promote").description(
|
|
|
13181
13681
|
reportError(err);
|
|
13182
13682
|
}
|
|
13183
13683
|
});
|
|
13684
|
+
var packCmd = program.command("pack").description("Pack-level publication and archive operations");
|
|
13685
|
+
packCmd.command("publish").description(
|
|
13686
|
+
"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."
|
|
13687
|
+
).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) => {
|
|
13688
|
+
try {
|
|
13689
|
+
const result = await publish({
|
|
13690
|
+
fromDir: opts.from,
|
|
13691
|
+
toDir: opts.to,
|
|
13692
|
+
operatorNotes: opts.operatorNotes,
|
|
13693
|
+
force: Boolean(opts.force),
|
|
13694
|
+
dryRun: Boolean(opts.dryRun)
|
|
13695
|
+
});
|
|
13696
|
+
if (result.dryRun) {
|
|
13697
|
+
process.stdout.write(`pack publish: DRY-RUN \u2014 no files written
|
|
13698
|
+
`);
|
|
13699
|
+
process.stdout.write(` package name: ${result.packageName}
|
|
13700
|
+
`);
|
|
13701
|
+
if (result.dryRunManifest) {
|
|
13702
|
+
const m = result.dryRunManifest;
|
|
13703
|
+
process.stdout.write(` topic: ${m.topic.slice(0, 80)}
|
|
13704
|
+
`);
|
|
13705
|
+
process.stdout.write(` frozen_at: ${m.frozen_at}
|
|
13706
|
+
`);
|
|
13707
|
+
process.stdout.write(` sections: ${m.totals.sections}
|
|
13708
|
+
`);
|
|
13709
|
+
process.stdout.write(` accepted: ${m.totals.accepted_claims}
|
|
13710
|
+
`);
|
|
13711
|
+
process.stdout.write(` receipt sha256:${m.freeze_receipt_sha256.slice(0, 16)}\u2026
|
|
13712
|
+
`);
|
|
13713
|
+
}
|
|
13714
|
+
return;
|
|
13715
|
+
}
|
|
13716
|
+
process.stdout.write(`pack publish: DONE
|
|
13717
|
+
`);
|
|
13718
|
+
process.stdout.write(` package name: ${result.packageName}
|
|
13719
|
+
`);
|
|
13720
|
+
process.stdout.write(` files written: ${result.filesWritten.length}
|
|
13721
|
+
`);
|
|
13722
|
+
for (const f of result.filesWritten) process.stdout.write(` ${f}
|
|
13723
|
+
`);
|
|
13724
|
+
process.stdout.write(` verify: ${result.verifyPassed ? "PASS" : "FAIL"}
|
|
13725
|
+
`);
|
|
13726
|
+
if (result.warnings.length > 0) {
|
|
13727
|
+
process.stdout.write(`
|
|
13728
|
+
warnings:
|
|
13729
|
+
`);
|
|
13730
|
+
for (const w of result.warnings) process.stdout.write(` - ${w}
|
|
13731
|
+
`);
|
|
13732
|
+
}
|
|
13733
|
+
if (!result.verifyPassed) process.exitCode = 2;
|
|
13734
|
+
} catch (err) {
|
|
13735
|
+
reportError(err);
|
|
13736
|
+
}
|
|
13737
|
+
});
|
|
13184
13738
|
program.parseAsync(process.argv);
|
|
13185
13739
|
//# sourceMappingURL=cli.js.map
|