rulereceipt 0.1.44 → 0.1.45

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
@@ -27,6 +27,7 @@ import { verifySessionHash } from "./verifyHash.js";
27
27
  import { saveEmailConfig, loadEmailConfig, detectSmtpHost, isValidEmail } from "./emailConfig.js";
28
28
  import { sendReportEmail } from "./sendReport.js";
29
29
  import { appendHistory, readHistorySince } from "./history.js";
30
+ import { maybeShowWhatsNew } from "./whatsNew.js";
30
31
  import { generateDigest } from "./digest.js";
31
32
  import { enableSchedule, disableSchedule, scheduleStatus } from "./schedule.js";
32
33
  import { findSplitBrainConflicts } from "./checks/splitBrain.js";
@@ -319,6 +320,12 @@ async function runCheck(opts) {
319
320
  console.log(`\n(${stale.length} saved correction${stale.length === 1 ? "" : "s"} no longer match any rule in this project — the rule was probably reworded. Run \`rulereceipt rules --list\` to see them.)`);
320
321
  }
321
322
  appendHistory(results, sessionFilePath);
323
+ // A once-per-update footer so a returning user sees the tool improved and
324
+ // comes back. Offline (notes ship in the package), fails open, and never
325
+ // on --markdown (that output is meant to be pasted into a PR/Slack).
326
+ if (!markdown) {
327
+ maybeShowWhatsNew(pkg.version);
328
+ }
322
329
  if (share) {
323
330
  await shareResults(results);
324
331
  }
@@ -0,0 +1,41 @@
1
+ export interface Release {
2
+ version: string;
3
+ highlights: string[];
4
+ }
5
+ /**
6
+ * User-facing release highlights, newest first.
7
+ *
8
+ * This is NOT the internal CHANGELOG (developer/business-facing, never
9
+ * shipped). It is the short, plain "here is what got better" note a user
10
+ * sees once, the first time they run a new version. Add ONE entry at the
11
+ * top on each release: what changed, in a sentence, honest, no marketing.
12
+ *
13
+ * The whole point: someone who bounced off an early version sees the tool
14
+ * is improving and comes back. So keep it truthful — a highlight that
15
+ * overstates is the exact failure this tool exists to catch.
16
+ */
17
+ export declare const RELEASES: Release[];
18
+ export declare function readLastSeen(): string | null;
19
+ export declare function writeLastSeen(version: string): void;
20
+ /**
21
+ * Numeric dotted-version compare: <0 if a<b, 0 if equal, >0 if a>b.
22
+ * Numeric per segment, so 0.1.9 < 0.1.10 (not lexical). Junk gives 0, which
23
+ * makes the caller show nothing rather than guess.
24
+ */
25
+ export declare function compareVersions(a: string, b: string): number;
26
+ /**
27
+ * Releases strictly newer than lastSeen and no newer than current, newest
28
+ * first. A null lastSeen (first run ever) yields nothing on purpose: a
29
+ * first-timer should see their report, not a changelog.
30
+ */
31
+ export declare function highlightsBetween(lastSeen: string | null, current: string, releases?: Release[]): Release[];
32
+ export declare function renderWhatsNew(releases: Release[], current: string): string;
33
+ /**
34
+ * Prints the "what's new" note once per new version, then records the
35
+ * current version so it never repeats for that version.
36
+ *
37
+ * Fails open, always: any error here must never affect the report the user
38
+ * actually ran for, and there is no network call — the notes ship inside
39
+ * the package, so the tool stays true to "nothing leaves your machine".
40
+ */
41
+ export declare function maybeShowWhatsNew(current: string, log?: (s: string) => void): void;
@@ -0,0 +1,112 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * User-facing release highlights, newest first.
6
+ *
7
+ * This is NOT the internal CHANGELOG (developer/business-facing, never
8
+ * shipped). It is the short, plain "here is what got better" note a user
9
+ * sees once, the first time they run a new version. Add ONE entry at the
10
+ * top on each release: what changed, in a sentence, honest, no marketing.
11
+ *
12
+ * The whole point: someone who bounced off an early version sees the tool
13
+ * is improving and comes back. So keep it truthful — a highlight that
14
+ * overstates is the exact failure this tool exists to catch.
15
+ */
16
+ export const RELEASES = [
17
+ { version: "0.1.45", highlights: ["The tool now shows what's improved since you last ran it, like this note."] },
18
+ { version: "0.1.44", highlights: ["The report now offers to install enforcement, but only when a rule was actually broken."] },
19
+ { version: "0.1.43", highlights: ["New check: a claim to have read or verified something, with nothing in the session behind it."] },
20
+ { version: "0.1.41", highlights: ["Emoji rules are checked properly now (Unicode properties, not a hand-written list)."] },
21
+ { version: "0.1.39", highlights: ["You can mark which clause in a rule is the actual prohibition, so only that blocks."] },
22
+ { version: "0.1.36", highlights: ["Enforcement arrives: the tool can act on a broken rule with a hook, not just report it."] },
23
+ ];
24
+ function stateDir() {
25
+ return join(homedir(), ".rulereceipt");
26
+ }
27
+ function lastSeenPath() {
28
+ return join(stateDir(), "last-seen-version");
29
+ }
30
+ export function readLastSeen() {
31
+ try {
32
+ const v = readFileSync(lastSeenPath(), "utf-8").trim();
33
+ return v.length > 0 ? v : null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function writeLastSeen(version) {
40
+ try {
41
+ const dir = stateDir();
42
+ if (!existsSync(dir))
43
+ mkdirSync(dir, { recursive: true });
44
+ writeFileSync(lastSeenPath(), version, "utf-8");
45
+ }
46
+ catch {
47
+ // best-effort; a run that cannot persist this just shows the note again
48
+ }
49
+ }
50
+ /**
51
+ * Numeric dotted-version compare: <0 if a<b, 0 if equal, >0 if a>b.
52
+ * Numeric per segment, so 0.1.9 < 0.1.10 (not lexical). Junk gives 0, which
53
+ * makes the caller show nothing rather than guess.
54
+ */
55
+ export function compareVersions(a, b) {
56
+ const pa = a.split(".").map((n) => parseInt(n, 10));
57
+ const pb = b.split(".").map((n) => parseInt(n, 10));
58
+ const len = Math.max(pa.length, pb.length);
59
+ for (let i = 0; i < len; i++) {
60
+ const x = pa[i] ?? 0;
61
+ const y = pb[i] ?? 0;
62
+ if (Number.isNaN(x) || Number.isNaN(y))
63
+ return 0;
64
+ if (x !== y)
65
+ return x - y;
66
+ }
67
+ return 0;
68
+ }
69
+ /**
70
+ * Releases strictly newer than lastSeen and no newer than current, newest
71
+ * first. A null lastSeen (first run ever) yields nothing on purpose: a
72
+ * first-timer should see their report, not a changelog.
73
+ */
74
+ export function highlightsBetween(lastSeen, current, releases = RELEASES) {
75
+ if (!lastSeen)
76
+ return [];
77
+ return releases.filter((r) => compareVersions(r.version, lastSeen) > 0 && compareVersions(r.version, current) <= 0);
78
+ }
79
+ export function renderWhatsNew(releases, current) {
80
+ const lines = [];
81
+ lines.push(`\n✨ What's new since you last ran rulereceipt (you're on v${current}):`);
82
+ for (const r of releases) {
83
+ for (const h of r.highlights) {
84
+ lines.push(` • v${r.version} ${h}`);
85
+ }
86
+ }
87
+ lines.push(`\nThis note shows once per update. To stay current: npx rulereceipt@latest`);
88
+ return lines.join("\n");
89
+ }
90
+ /**
91
+ * Prints the "what's new" note once per new version, then records the
92
+ * current version so it never repeats for that version.
93
+ *
94
+ * Fails open, always: any error here must never affect the report the user
95
+ * actually ran for, and there is no network call — the notes ship inside
96
+ * the package, so the tool stays true to "nothing leaves your machine".
97
+ */
98
+ export function maybeShowWhatsNew(current, log = console.log) {
99
+ try {
100
+ const lastSeen = readLastSeen();
101
+ const news = highlightsBetween(lastSeen, current, RELEASES);
102
+ if (news.length > 0)
103
+ log(renderWhatsNew(news, current));
104
+ // Record current even on the first run and even when nothing showed, so
105
+ // the next update is measured from here.
106
+ if (lastSeen !== current)
107
+ writeLastSeen(current);
108
+ }
109
+ catch {
110
+ // never let a footer break the run
111
+ }
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulereceipt",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "description": "Checks whether a Claude Code session actually followed your CLAUDE.md / AGENTS.md rules, with evidence.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -57,6 +57,6 @@
57
57
  "tsx": "^4.19.0",
58
58
  "typescript": "^5.6.0",
59
59
  "typescript-eslint": "^8.68.0",
60
- "vitest": "^4.1.11"
60
+ "vitest": "^5.0.1"
61
61
  }
62
62
  }