@rulvar/cli 1.2.0 → 1.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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as resumeCommand, c as driveRun, d as renderEventLine, f as DEFAULT_STORE_DIR, g as looksLikeFile, h as loadWorkflowModule, i as inspectCommand, l as reportOutcome, m as loadCliConfig, n as HELP, o as runCommand, p as assembleEngine, r as runCli, s as runsLsCommand, t as processIo, u as attachProgress } from "./io-poGpn70r.js";
1
+ import { a as resumeCommand, c as driveRun, d as renderEventLine, f as DEFAULT_STORE_DIR, g as looksLikeFile, h as loadWorkflowModule, i as inspectCommand, l as reportOutcome, m as loadCliConfig, n as HELP, o as runCommand, p as assembleEngine, r as runCli, s as runsLsCommand, t as processIo, u as attachProgress } from "./io-DSpNsEKg.js";
2
2
  import { ConfigError, InvalidResolutionError, JournalCompatibilityError, LeaseHeldError, Replayer, RulvarError, buildDeriverRegistry, costReportFromJournal, maskSecrets, normalizeEntry, scanJournalCompatibility, validateSchemaSpec } from "@rulvar/core";
3
3
  //#region src/server.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { ConfigError, FileModelKnowledgeStore, JsonlFileStore, claimExpired, compilePermissionPreset, costReportFromJournal, createEngine, parseModelRef, priceUsdOf, remeasureQueue, resolvePricing, runProfile } from "@rulvar/core";
1
+ import { ConfigError, FileModelKnowledgeStore, INBOX_PROPOSAL_TTL_DAYS, JsonlFileStore, claimExpired, claimExpiry, compilePermissionPreset, costReportFromJournal, createEngine, parseModelRef, priceUsdOf, proposalStatement, remeasureQueue, resolvePricing, runProfile } from "@rulvar/core";
2
2
  import { parseArgs } from "node:util";
3
3
  import { join, resolve } from "node:path";
4
4
  import { existsSync, statSync } from "node:fs";
@@ -487,15 +487,14 @@ async function planCommand(argv, context) {
487
487
  * consumption path. Claims with full provenance for the humans who
488
488
  * author ladders, floors, and profiles; no run and no pin, so model
489
489
  * names render VERBATIM here (only in-run cards are nameless). Reads
490
- * the per-project file store (./rulvar.models.json). The grammar
491
- * members inbox (phase 3) and sweep (phase 2) fail loudly until their
492
- * phases ship.
490
+ * the per-project file store (./rulvar.models.json).
493
491
  */
494
492
  async function kbCommand(argv, context) {
495
493
  const [sub, ...rest] = argv;
496
- if (sub === "inbox") throw new ConfigError("rulvar kb inbox arrives with ModelKnowledge phase 3 (M12, gated by the measured-value checkpoint; docs/05, section \"Phases and placement\")");
494
+ if (sub === "inbox") return await kbInboxCommand(rest, context);
495
+ if (sub === "gate") return await kbGateCommand(rest, context);
497
496
  if (sub === "sweep") return await kbSweepCommand(rest, context);
498
- if (sub !== "list" || rest.length > 0) throw new ConfigError("usage: rulvar kb <list | inbox | sweep> (no aliases in v1)");
497
+ if (sub !== "list" || rest.length > 0) throw new ConfigError("usage: rulvar kb <list | inbox | gate | sweep> (no aliases in v1)");
499
498
  const snapshot = await new FileModelKnowledgeStore({ path: join(context.cwd, "rulvar.models.json") }).current();
500
499
  context.io.out(`knowledge store: rulvar.models.json (version ${String(snapshot.version)}, ${String(snapshot.claims.length)} claim${snapshot.claims.length === 1 ? "" : "s"})`);
501
500
  renderKbList(snapshot, context);
@@ -517,6 +516,248 @@ function renderKbList(snapshot, context) {
517
516
  }
518
517
  }
519
518
  /**
519
+ * rulvar kb inbox (M12-T03): aggregates kb_propose-born proposals from
520
+ * FINISHED runs through the RunLedger fold behind the LedgerExport
521
+ * seam. Grouping of matching (subject, taskClass, polarity) triples is
522
+ * STRICTLY display: the command writes nothing, authorizes no spend,
523
+ * and schedules no sweeps; gating a proposal into a claim is the
524
+ * separate human gate flow. The age anchor is the run's terminal
525
+ * updatedAt (journal entries carry no wall clock by design): proposals
526
+ * of runs finished more than fourteen days ago expire out of the view.
527
+ * This is the human review surface, so the quarantined note text and
528
+ * concrete model names render here VERBATIM, exactly like kb list.
529
+ */
530
+ async function kbInboxCommand(argv, context) {
531
+ const flags = parseCommonFlags(argv);
532
+ if (flags.positionals.length > 0) throw new ConfigError("usage: rulvar kb inbox [--store PATH]");
533
+ let plan;
534
+ try {
535
+ plan = await import("./dist-Clz0iBp5.js");
536
+ } catch {
537
+ throw new ConfigError("rulvar kb inbox requires @rulvar/plan (the RunLedger fold behind the LedgerExport seam)");
538
+ }
539
+ const assembled = assembleEngine({
540
+ config: await loadCliConfig(context.cwd),
541
+ ...flags.store === void 0 ? {} : { storePath: flags.store },
542
+ cwd: context.cwd
543
+ });
544
+ const finished = (await assembled.store.listRuns()).filter((meta) => meta.status !== "running");
545
+ const cutoffMs = Date.now() - INBOX_PROPOSAL_TTL_DAYS * 24 * 60 * 60 * 1e3;
546
+ const groups = /* @__PURE__ */ new Map();
547
+ let expired = 0;
548
+ for (const meta of finished) {
549
+ const entries = await assembled.store.load(meta.runId);
550
+ const view = plan.foldLedger(entries, {
551
+ ledgerScope: "",
552
+ planScope: "plan"
553
+ });
554
+ for (const row of view.observations) {
555
+ if (row.subject === void 0 || row.polarity === void 0 || row.trigger === void 0) continue;
556
+ if (Date.parse(meta.updatedAt) < cutoffMs) {
557
+ expired += 1;
558
+ continue;
559
+ }
560
+ const key = [
561
+ row.subject.model,
562
+ row.subject.effort ?? "",
563
+ row.taskClass,
564
+ row.polarity
565
+ ].join("|");
566
+ const group = groups.get(key) ?? {
567
+ subject: row.subject,
568
+ taskClass: row.taskClass,
569
+ polarity: row.polarity,
570
+ members: []
571
+ };
572
+ group.members.push({
573
+ runId: meta.runId,
574
+ runLabel: meta.name ?? meta.workflowName ?? "",
575
+ entryRef: row.entryRef,
576
+ logicalTaskId: row.logicalTaskId,
577
+ ...row.tierObserved === void 0 ? {} : { tierObserved: row.tierObserved },
578
+ trigger: row.trigger,
579
+ note: row.note,
580
+ evidenceRefs: row.evidenceRefs,
581
+ finishedAt: meta.updatedAt
582
+ });
583
+ groups.set(key, group);
584
+ }
585
+ }
586
+ const total = [...groups.values()].reduce((sum, group) => sum + group.members.length, 0);
587
+ context.io.out(`kb inbox: ${String(total)} live proposal${total === 1 ? "" : "s"} in ${String(groups.size)} group${groups.size === 1 ? "" : "s"} across ${String(finished.length)} finished run${finished.length === 1 ? "" : "s"}` + (expired > 0 ? `; ${String(expired)} expired (older than 14 days)` : ""));
588
+ for (const key of [...groups.keys()].sort()) {
589
+ const group = groups.get(key);
590
+ const effort = group.subject.effort === void 0 ? "" : ` effort=${group.subject.effort}`;
591
+ context.io.out(`${group.subject.model}${effort} :: ${group.taskClass} ${group.polarity} (${String(group.members.length)} proposal${group.members.length === 1 ? "" : "s"})`);
592
+ context.io.out(` statement: ${proposalStatement({
593
+ taskClass: group.taskClass,
594
+ polarity: group.polarity,
595
+ trigger: group.members[0].trigger
596
+ })}`);
597
+ for (const member of group.members) {
598
+ const tier = member.tierObserved === void 0 ? "" : ` tier=${String(member.tierObserved)}`;
599
+ const label = member.runLabel === "" ? "" : ` (${member.runLabel})`;
600
+ context.io.out(` - run=${member.runId}${label}#${String(member.entryRef)}${tier} trigger=${member.trigger} lineage=${member.logicalTaskId} finished=${member.finishedAt}`);
601
+ if (member.note !== "") context.io.out(` note: ${member.note}`);
602
+ if (member.evidenceRefs.length > 0) context.io.out(` evidence: ${member.evidenceRefs.map((ref) => `#${String(ref)}`).join(", ")}`);
603
+ }
604
+ }
605
+ return 0;
606
+ }
607
+ const RULED_OUT_VOCABULARY = [
608
+ "prompt",
609
+ "tools",
610
+ "difficulty",
611
+ "transient-provider"
612
+ ];
613
+ /**
614
+ * rulvar kb gate (M12-T04): the human gate turning ONE inbox proposal
615
+ * into a human-editorial claim. The attribution attestation is
616
+ * mandatory by construction: without --ruled-out the GateRecord does
617
+ * not assemble and nothing is written. The born claim carries the
618
+ * typed template statement (never the quarantined note), the origin
619
+ * provenance back to the proposing run, evidence resolving into that
620
+ * run's journal, and the editorial TTL; the commit is CAS against the
621
+ * per-project file store, whose git review is the authenticating gate.
622
+ */
623
+ async function kbGateCommand(argv, context) {
624
+ const { values, positionals } = parseArgs({
625
+ args: argv,
626
+ allowPositionals: true,
627
+ options: {
628
+ store: { type: "string" },
629
+ approver: { type: "string" },
630
+ "ruled-out": { type: "string" },
631
+ "contrast-run": { type: "string" },
632
+ "contrast-eval": { type: "string" },
633
+ confidence: { type: "string" }
634
+ }
635
+ });
636
+ const usage = "usage: rulvar kb gate <runId> <entryRef> --approver NAME --ruled-out a,b,c [--contrast-run runId#seq | --contrast-eval reportId:caseId[,caseId...]] [--confidence high|medium|low] [--store PATH]";
637
+ const runId = positionals[0];
638
+ const entryRefRaw = positionals[1];
639
+ if (runId === void 0 || entryRefRaw === void 0 || positionals.length > 2) throw new ConfigError(usage);
640
+ const entryRef = Number(entryRefRaw);
641
+ if (!Number.isInteger(entryRef) || entryRef < 1) throw new ConfigError(`entryRef must be a positive integer entry seq, got '${entryRefRaw}'`);
642
+ const approver = values.approver;
643
+ if (approver === void 0 || approver === "") throw new ConfigError(`--approver is required: the attestation names its human. ${usage}`);
644
+ const ruledOutRaw = values["ruled-out"];
645
+ if (ruledOutRaw === void 0 || ruledOutRaw === "") throw new ConfigError(`--ruled-out is required: the attribution attestation lists the alternative causes you ruled out (${RULED_OUT_VOCABULARY.join(", ")}). ${usage}`);
646
+ const ruledOut = ruledOutRaw.split(",").map((entry) => entry.trim());
647
+ for (const entry of ruledOut) if (!RULED_OUT_VOCABULARY.includes(entry)) throw new ConfigError(`--ruled-out '${entry}' is not in the attestation vocabulary (${RULED_OUT_VOCABULARY.join(", ")})`);
648
+ if (values["contrast-run"] !== void 0 && values["contrast-eval"] !== void 0) throw new ConfigError("--contrast-run and --contrast-eval are mutually exclusive");
649
+ let contrastEvidence;
650
+ if (values["contrast-run"] !== void 0) {
651
+ const [contrastRun, seqRaw, ...tail] = values["contrast-run"].split("#");
652
+ const seq = Number(seqRaw);
653
+ if (contrastRun === void 0 || contrastRun === "" || tail.length > 0 || !Number.isInteger(seq) || seq < 1) throw new ConfigError("--contrast-run must look like 'runId#seq'");
654
+ contrastEvidence = {
655
+ kind: "journal",
656
+ runId: contrastRun,
657
+ entryRef: seq
658
+ };
659
+ }
660
+ if (values["contrast-eval"] !== void 0) {
661
+ const [reportId, caseList, ...tail] = values["contrast-eval"].split(":");
662
+ const caseIds = (caseList ?? "").split(",").filter((entry) => entry !== "");
663
+ if (reportId === void 0 || reportId === "" || tail.length > 0 || caseIds.length === 0) throw new ConfigError("--contrast-eval must look like 'reportId:caseId[,caseId...]'");
664
+ contrastEvidence = {
665
+ kind: "eval",
666
+ reportId,
667
+ caseIds
668
+ };
669
+ }
670
+ const confidence = values.confidence ?? "medium";
671
+ if (![
672
+ "high",
673
+ "medium",
674
+ "low"
675
+ ].includes(confidence)) throw new ConfigError(`--confidence must be high, medium or low, got '${String(values.confidence)}'`);
676
+ let plan;
677
+ try {
678
+ plan = await import("./dist-Clz0iBp5.js");
679
+ } catch {
680
+ throw new ConfigError("rulvar kb gate requires @rulvar/plan (the RunLedger fold behind the LedgerExport seam)");
681
+ }
682
+ const assembled = assembleEngine({
683
+ config: await loadCliConfig(context.cwd),
684
+ ...values.store === void 0 ? {} : { storePath: values.store },
685
+ cwd: context.cwd
686
+ });
687
+ const meta = (await assembled.store.listRuns()).find((candidate) => candidate.runId === runId);
688
+ if (meta === void 0) throw new ConfigError(`run '${runId}' not found in the store`);
689
+ if (meta.status === "running") throw new ConfigError(`run '${runId}' is still running; proposals gate from finished runs`);
690
+ if (Date.parse(meta.updatedAt) < Date.now() - INBOX_PROPOSAL_TTL_DAYS * 24 * 60 * 60 * 1e3) throw new ConfigError(`the proposal expired: run '${runId}' finished ${meta.updatedAt}, and inbox entries expire after ${String(INBOX_PROPOSAL_TTL_DAYS)} days`);
691
+ const entries = await assembled.store.load(runId);
692
+ const proposal = plan.foldLedger(entries, {
693
+ ledgerScope: "",
694
+ planScope: "plan"
695
+ }).observations.find((row) => row.entryRef === entryRef);
696
+ if (proposal === void 0 || proposal.subject === void 0 || proposal.polarity === void 0 || proposal.trigger === void 0) throw new ConfigError(`run '${runId}' entry ${String(entryRef)} is not a kb_propose proposal (see rulvar kb inbox for the gateable entries)`);
697
+ const store = new FileModelKnowledgeStore({ path: join(context.cwd, "rulvar.models.json") });
698
+ const snapshot = await store.current();
699
+ const already = snapshot.claims.find((claim) => claim.origin?.kind === "kb-proposal" && claim.origin.runId === runId && claim.origin.entryRef === entryRef && claim.status === "active");
700
+ if (already !== void 0) throw new ConfigError(`this proposal is already gated as claim '${already.id}' (supersede is the edit path)`);
701
+ const observedAt = meta.updatedAt;
702
+ const evidence = proposal.evidenceRefs.length > 0 ? proposal.evidenceRefs.map((ref) => ({
703
+ kind: "journal",
704
+ runId,
705
+ entryRef: ref
706
+ })) : [{
707
+ kind: "journal",
708
+ runId,
709
+ entryRef
710
+ }];
711
+ const claim = {
712
+ id: `kb-proposal-${runId}-${String(entryRef)}`,
713
+ subject: {
714
+ model: proposal.subject.model,
715
+ ...proposal.subject.effort === void 0 ? {} : { effort: proposal.subject.effort }
716
+ },
717
+ taskClass: proposal.taskClass,
718
+ polarity: proposal.polarity,
719
+ statement: proposalStatement({
720
+ taskClass: proposal.taskClass,
721
+ polarity: proposal.polarity,
722
+ trigger: proposal.trigger
723
+ }),
724
+ class: "human-editorial",
725
+ status: "active",
726
+ evidence,
727
+ confidence,
728
+ observedAt,
729
+ expiresAt: claimExpiry("human-editorial", proposal.polarity, observedAt),
730
+ author: {
731
+ kind: "human",
732
+ id: approver
733
+ },
734
+ origin: {
735
+ kind: "kb-proposal",
736
+ runId,
737
+ entryRef
738
+ }
739
+ };
740
+ const gate = {
741
+ kind: "human",
742
+ approver,
743
+ at: (/* @__PURE__ */ new Date()).toISOString(),
744
+ attribution: {
745
+ ruledOut,
746
+ ...contrastEvidence === void 0 ? {} : { contrastEvidence }
747
+ }
748
+ };
749
+ const version = await store.commit([{
750
+ op: "add",
751
+ claim,
752
+ gate
753
+ }], snapshot.version);
754
+ context.io.out(`gated: ${claim.id} (store version ${String(version)}); the git review of rulvar.models.json is the authenticating gate`);
755
+ context.io.out(` ${claim.subject.model}${claim.subject.effort === void 0 ? "" : ` effort=${claim.subject.effort}`} :: ${claim.taskClass} ${claim.polarity}`);
756
+ context.io.out(` ${claim.statement}`);
757
+ context.io.out(` origin: kb-proposal run=${runId}#${String(entryRef)} expires=${claim.expiresAt}`);
758
+ return 0;
759
+ }
760
+ /**
520
761
  * rulvar kb sweep (M11-T05):
521
762
  * falsification sweeps, run manually, from CI, or from a user cron,
522
763
  * NEVER engine-scheduled. The matrix is the config's FIXED pool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "rulvar shell: run/resume/runs/inspect/plan/kb commands, TUI progress, createServer, createWorker, OTel exporter.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,17 +22,17 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@rulvar/core": "1.2.0"
25
+ "@rulvar/core": "1.3.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.20.0",
29
29
  "tsdown": "^0.22.3",
30
30
  "typescript": "~6.0.3",
31
- "@rulvar/testing": "1.2.0",
32
- "@rulvar/store-sqlite": "1.2.0",
33
- "@rulvar/planner": "1.2.0",
34
- "@rulvar/evals": "1.2.0",
35
- "@rulvar/plan": "1.2.0"
31
+ "@rulvar/testing": "1.3.0",
32
+ "@rulvar/store-sqlite": "1.3.0",
33
+ "@rulvar/planner": "1.3.0",
34
+ "@rulvar/plan": "1.3.0",
35
+ "@rulvar/evals": "1.3.0"
36
36
  },
37
37
  "bin": {
38
38
  "rulvar": "./dist/cli.js"