mneme-ai 0.24.0 → 0.25.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.
Files changed (57) hide show
  1. package/dist/iris/adaptive.d.ts +40 -0
  2. package/dist/iris/adaptive.d.ts.map +1 -0
  3. package/dist/iris/adaptive.js +131 -0
  4. package/dist/iris/adaptive.js.map +1 -0
  5. package/dist/iris/adaptive.test.d.ts +2 -0
  6. package/dist/iris/adaptive.test.d.ts.map +1 -0
  7. package/dist/iris/adaptive.test.js +113 -0
  8. package/dist/iris/adaptive.test.js.map +1 -0
  9. package/dist/iris/contract.d.ts +28 -0
  10. package/dist/iris/contract.d.ts.map +1 -0
  11. package/dist/iris/contract.js +98 -0
  12. package/dist/iris/contract.js.map +1 -0
  13. package/dist/iris/contract.test.d.ts +2 -0
  14. package/dist/iris/contract.test.d.ts.map +1 -0
  15. package/dist/iris/contract.test.js +82 -0
  16. package/dist/iris/contract.test.js.map +1 -0
  17. package/dist/iris/entity.d.ts +76 -0
  18. package/dist/iris/entity.d.ts.map +1 -0
  19. package/dist/iris/entity.js +109 -0
  20. package/dist/iris/entity.js.map +1 -0
  21. package/dist/iris/entity.test.d.ts +2 -0
  22. package/dist/iris/entity.test.d.ts.map +1 -0
  23. package/dist/iris/entity.test.js +118 -0
  24. package/dist/iris/entity.test.js.map +1 -0
  25. package/dist/iris/flash.d.ts +21 -0
  26. package/dist/iris/flash.d.ts.map +1 -0
  27. package/dist/iris/flash.js +124 -0
  28. package/dist/iris/flash.js.map +1 -0
  29. package/dist/iris/flash.test.d.ts +2 -0
  30. package/dist/iris/flash.test.d.ts.map +1 -0
  31. package/dist/iris/flash.test.js +67 -0
  32. package/dist/iris/flash.test.js.map +1 -0
  33. package/dist/iris/headline.d.ts +59 -0
  34. package/dist/iris/headline.d.ts.map +1 -0
  35. package/dist/iris/headline.js +288 -0
  36. package/dist/iris/headline.js.map +1 -0
  37. package/dist/iris/headline.test.d.ts +2 -0
  38. package/dist/iris/headline.test.d.ts.map +1 -0
  39. package/dist/iris/headline.test.js +248 -0
  40. package/dist/iris/headline.test.js.map +1 -0
  41. package/dist/iris/index.d.ts +34 -0
  42. package/dist/iris/index.d.ts.map +1 -0
  43. package/dist/iris/index.js +30 -0
  44. package/dist/iris/index.js.map +1 -0
  45. package/dist/iris/iris.integration.test.d.ts +2 -0
  46. package/dist/iris/iris.integration.test.d.ts.map +1 -0
  47. package/dist/iris/iris.integration.test.js +102 -0
  48. package/dist/iris/iris.integration.test.js.map +1 -0
  49. package/dist/iris/pyramid.d.ts +41 -0
  50. package/dist/iris/pyramid.d.ts.map +1 -0
  51. package/dist/iris/pyramid.js +181 -0
  52. package/dist/iris/pyramid.js.map +1 -0
  53. package/dist/iris/pyramid.test.d.ts +2 -0
  54. package/dist/iris/pyramid.test.d.ts.map +1 -0
  55. package/dist/iris/pyramid.test.js +199 -0
  56. package/dist/iris/pyramid.test.js.map +1 -0
  57. package/package.json +5 -5
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Iris — adaptive verbosity per user history.
3
+ *
4
+ * Goal: first-time users of `mneme forensics` see the verbose "how to read"
5
+ * guide. After they've run it 5+ times, we trust they know — collapse the
6
+ * guide to a one-liner.
7
+ *
8
+ * State lives in `<repoRoot>/.mneme/iris-state.json` (sibling to session.json).
9
+ * Atomic writes via temp-file rename.
10
+ *
11
+ * Pure-ish: state is read/written through a small, file-only API. All policy
12
+ * decisions (`shouldShowVerboseGuide`, etc.) are pure functions of the state.
13
+ */
14
+ export interface IrisState {
15
+ /** Command name → run count. */
16
+ commandsRun: Record<string, number>;
17
+ /** Toggled true after 5+ uses of any one command. */
18
+ preferTerse: boolean;
19
+ /** Command → ISO timestamp of most recent run. */
20
+ lastSeen: Record<string, string>;
21
+ }
22
+ /** Threshold above which we start showing the terse output. */
23
+ export declare const VERBOSE_GUIDE_THRESHOLD = 5;
24
+ /** Read state — returns a clean default if missing or corrupt. */
25
+ export declare function readIrisState(repoRoot: string): IrisState;
26
+ /** Persist state atomically. */
27
+ export declare function writeIrisState(repoRoot: string, state: IrisState): void;
28
+ /** Increment the run-count for a command and persist. Best-effort. */
29
+ export declare function recordCommandRun(repoRoot: string, command: string): void;
30
+ /**
31
+ * Should we show the verbose how-to-read guide for this command?
32
+ *
33
+ * True when the user has run the command fewer than 5 times. We do NOT
34
+ * gate on `preferTerse` for the per-command decision — preferTerse only
35
+ * influences shared chrome (banners, footers).
36
+ */
37
+ export declare function shouldShowVerboseGuide(state: IrisState, command: string): boolean;
38
+ /** Reset state — used by tests and `mneme reset-iris`. */
39
+ export declare function clearIrisState(repoRoot: string): void;
40
+ //# sourceMappingURL=adaptive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adaptive.d.ts","sourceRoot":"","sources":["../../src/iris/adaptive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,MAAM,WAAW,SAAS;IACxB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,qDAAqD;IACrD,WAAW,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,+DAA+D;AAC/D,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAUzC,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAmCzD;AAwBD,gCAAgC;AAChC,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAEvE;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAYxE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAGjF;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQrD"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Iris — adaptive verbosity per user history.
3
+ *
4
+ * Goal: first-time users of `mneme forensics` see the verbose "how to read"
5
+ * guide. After they've run it 5+ times, we trust they know — collapse the
6
+ * guide to a one-liner.
7
+ *
8
+ * State lives in `<repoRoot>/.mneme/iris-state.json` (sibling to session.json).
9
+ * Atomic writes via temp-file rename.
10
+ *
11
+ * Pure-ish: state is read/written through a small, file-only API. All policy
12
+ * decisions (`shouldShowVerboseGuide`, etc.) are pure functions of the state.
13
+ */
14
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ /** Threshold above which we start showing the terse output. */
17
+ export const VERBOSE_GUIDE_THRESHOLD = 5;
18
+ function statePath(repoRoot) {
19
+ return join(repoRoot, ".mneme", "iris-state.json");
20
+ }
21
+ function emptyState() {
22
+ return { commandsRun: {}, preferTerse: false, lastSeen: {} };
23
+ }
24
+ /** Read state — returns a clean default if missing or corrupt. */
25
+ export function readIrisState(repoRoot) {
26
+ const path = statePath(repoRoot);
27
+ if (!existsSync(path))
28
+ return emptyState();
29
+ let raw;
30
+ try {
31
+ raw = readFileSync(path, "utf8");
32
+ }
33
+ catch {
34
+ return emptyState();
35
+ }
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(raw);
39
+ }
40
+ catch {
41
+ return emptyState();
42
+ }
43
+ if (!parsed || typeof parsed !== "object")
44
+ return emptyState();
45
+ const s = parsed;
46
+ const commandsRun = {};
47
+ if (s.commandsRun && typeof s.commandsRun === "object") {
48
+ for (const [k, v] of Object.entries(s.commandsRun)) {
49
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0)
50
+ commandsRun[k] = v;
51
+ }
52
+ }
53
+ const lastSeen = {};
54
+ if (s.lastSeen && typeof s.lastSeen === "object") {
55
+ for (const [k, v] of Object.entries(s.lastSeen)) {
56
+ if (typeof v === "string")
57
+ lastSeen[k] = v;
58
+ }
59
+ }
60
+ return {
61
+ commandsRun,
62
+ preferTerse: typeof s.preferTerse === "boolean" ? s.preferTerse : false,
63
+ lastSeen,
64
+ };
65
+ }
66
+ function atomicWrite(path, contents) {
67
+ const dir = dirname(path);
68
+ if (!existsSync(dir)) {
69
+ try {
70
+ mkdirSync(dir, { recursive: true });
71
+ }
72
+ catch {
73
+ return;
74
+ }
75
+ }
76
+ const tmp = `${path}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
77
+ try {
78
+ writeFileSync(tmp, contents, "utf8");
79
+ renameSync(tmp, path);
80
+ }
81
+ catch {
82
+ try {
83
+ rmSync(tmp, { force: true });
84
+ }
85
+ catch {
86
+ /* ignore */
87
+ }
88
+ }
89
+ }
90
+ /** Persist state atomically. */
91
+ export function writeIrisState(repoRoot, state) {
92
+ atomicWrite(statePath(repoRoot), JSON.stringify(state, null, 2));
93
+ }
94
+ /** Increment the run-count for a command and persist. Best-effort. */
95
+ export function recordCommandRun(repoRoot, command) {
96
+ const state = readIrisState(repoRoot);
97
+ state.commandsRun[command] = (state.commandsRun[command] ?? 0) + 1;
98
+ state.lastSeen[command] = new Date().toISOString();
99
+ // Once any command has hit threshold, mark this user as a "power user".
100
+ for (const n of Object.values(state.commandsRun)) {
101
+ if (n >= VERBOSE_GUIDE_THRESHOLD) {
102
+ state.preferTerse = true;
103
+ break;
104
+ }
105
+ }
106
+ writeIrisState(repoRoot, state);
107
+ }
108
+ /**
109
+ * Should we show the verbose how-to-read guide for this command?
110
+ *
111
+ * True when the user has run the command fewer than 5 times. We do NOT
112
+ * gate on `preferTerse` for the per-command decision — preferTerse only
113
+ * influences shared chrome (banners, footers).
114
+ */
115
+ export function shouldShowVerboseGuide(state, command) {
116
+ const count = state.commandsRun[command] ?? 0;
117
+ return count < VERBOSE_GUIDE_THRESHOLD;
118
+ }
119
+ /** Reset state — used by tests and `mneme reset-iris`. */
120
+ export function clearIrisState(repoRoot) {
121
+ const path = statePath(repoRoot);
122
+ if (!existsSync(path))
123
+ return;
124
+ try {
125
+ rmSync(path, { force: true });
126
+ }
127
+ catch {
128
+ /* ignore */
129
+ }
130
+ }
131
+ //# sourceMappingURL=adaptive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adaptive.js","sourceRoot":"","sources":["../../src/iris/adaptive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adaptive.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adaptive.test.d.ts","sourceRoot":"","sources":["../../src/iris/adaptive.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, rmSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { VERBOSE_GUIDE_THRESHOLD, clearIrisState, readIrisState, recordCommandRun, shouldShowVerboseGuide, writeIrisState, } from "./adaptive.js";
6
+ let tmpDir;
7
+ beforeEach(() => {
8
+ tmpDir = mkdtempSync(join(tmpdir(), "mneme-iris-state-test-"));
9
+ });
10
+ afterEach(() => {
11
+ if (existsSync(tmpDir))
12
+ rmSync(tmpDir, { recursive: true, force: true });
13
+ });
14
+ describe("readIrisState", () => {
15
+ it("returns clean default when no state file exists", () => {
16
+ const s = readIrisState(tmpDir);
17
+ expect(s.commandsRun).toEqual({});
18
+ expect(s.lastSeen).toEqual({});
19
+ expect(s.preferTerse).toBe(false);
20
+ });
21
+ it("returns clean default when JSON is corrupt", () => {
22
+ mkdirSync(join(tmpDir, ".mneme"), { recursive: true });
23
+ writeFileSync(join(tmpDir, ".mneme", "iris-state.json"), "{not json", "utf8");
24
+ const s = readIrisState(tmpDir);
25
+ expect(s.commandsRun).toEqual({});
26
+ });
27
+ it("ignores malformed counts (negative / non-number)", () => {
28
+ mkdirSync(join(tmpDir, ".mneme"), { recursive: true });
29
+ writeFileSync(join(tmpDir, ".mneme", "iris-state.json"), JSON.stringify({
30
+ commandsRun: { good: 3, bad: "string", negative: -1 },
31
+ lastSeen: {},
32
+ preferTerse: false,
33
+ }), "utf8");
34
+ const s = readIrisState(tmpDir);
35
+ expect(s.commandsRun).toEqual({ good: 3 });
36
+ });
37
+ });
38
+ describe("recordCommandRun", () => {
39
+ it("increments count + writes timestamp", () => {
40
+ recordCommandRun(tmpDir, "ask");
41
+ let s = readIrisState(tmpDir);
42
+ expect(s.commandsRun.ask).toBe(1);
43
+ expect(s.lastSeen.ask).toBeTruthy();
44
+ recordCommandRun(tmpDir, "ask");
45
+ s = readIrisState(tmpDir);
46
+ expect(s.commandsRun.ask).toBe(2);
47
+ });
48
+ it("maintains separate counts per command", () => {
49
+ recordCommandRun(tmpDir, "ask");
50
+ recordCommandRun(tmpDir, "forensics");
51
+ recordCommandRun(tmpDir, "forensics");
52
+ const s = readIrisState(tmpDir);
53
+ expect(s.commandsRun.ask).toBe(1);
54
+ expect(s.commandsRun.forensics).toBe(2);
55
+ });
56
+ it("flips preferTerse to true once threshold is crossed", () => {
57
+ for (let i = 0; i < VERBOSE_GUIDE_THRESHOLD; i++) {
58
+ recordCommandRun(tmpDir, "ask");
59
+ }
60
+ const s = readIrisState(tmpDir);
61
+ expect(s.preferTerse).toBe(true);
62
+ });
63
+ it("preferTerse stays false until threshold", () => {
64
+ for (let i = 0; i < VERBOSE_GUIDE_THRESHOLD - 1; i++) {
65
+ recordCommandRun(tmpDir, "ask");
66
+ }
67
+ const s = readIrisState(tmpDir);
68
+ expect(s.preferTerse).toBe(false);
69
+ });
70
+ });
71
+ describe("shouldShowVerboseGuide", () => {
72
+ it("true for first-time users", () => {
73
+ const s = readIrisState(tmpDir);
74
+ expect(shouldShowVerboseGuide(s, "ask")).toBe(true);
75
+ });
76
+ it("true while count < threshold", () => {
77
+ const s = { commandsRun: { ask: 4 }, preferTerse: false, lastSeen: {} };
78
+ expect(shouldShowVerboseGuide(s, "ask")).toBe(true);
79
+ });
80
+ it("false when count ≥ threshold", () => {
81
+ const s = { commandsRun: { ask: 5 }, preferTerse: true, lastSeen: {} };
82
+ expect(shouldShowVerboseGuide(s, "ask")).toBe(false);
83
+ });
84
+ it("tracks per-command (other commands keep verbose)", () => {
85
+ const s = { commandsRun: { ask: 10 }, preferTerse: true, lastSeen: {} };
86
+ expect(shouldShowVerboseGuide(s, "ask")).toBe(false);
87
+ expect(shouldShowVerboseGuide(s, "forensics")).toBe(true);
88
+ });
89
+ });
90
+ describe("round-trip read/write", () => {
91
+ it("survives write+read roundtrip", () => {
92
+ const state = {
93
+ commandsRun: { a: 3, b: 7 },
94
+ lastSeen: { a: "2024-01-01T00:00:00Z", b: "2024-01-02T00:00:00Z" },
95
+ preferTerse: true,
96
+ };
97
+ writeIrisState(tmpDir, state);
98
+ const back = readIrisState(tmpDir);
99
+ expect(back).toEqual(state);
100
+ });
101
+ });
102
+ describe("clearIrisState", () => {
103
+ it("removes the state file", () => {
104
+ recordCommandRun(tmpDir, "ask");
105
+ expect(existsSync(join(tmpDir, ".mneme", "iris-state.json"))).toBe(true);
106
+ clearIrisState(tmpDir);
107
+ expect(existsSync(join(tmpDir, ".mneme", "iris-state.json"))).toBe(false);
108
+ });
109
+ it("is a no-op when no state file exists", () => {
110
+ expect(() => clearIrisState(tmpDir)).not.toThrow();
111
+ });
112
+ });
113
+ //# sourceMappingURL=adaptive.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adaptive.test.js","sourceRoot":"","sources":["../../src/iris/adaptive.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Iris — 30-second contract validator.
3
+ *
4
+ * Every Mneme command output should be scannable in ~30 seconds. This file
5
+ * encodes that as a checkable contract — both for tests ("does my command
6
+ * obey the contract?") and for users (`mneme verify-output`).
7
+ *
8
+ * Pure function. No I/O.
9
+ */
10
+ /** Strip ANSI escapes for visible-width math. Local copy to avoid coupling. */
11
+ export declare function stripAnsi(s: string): string;
12
+ export interface ContractResult {
13
+ ok: boolean;
14
+ violations: string[];
15
+ }
16
+ /**
17
+ * Validate that a rendered output meets the 30-second readability contract.
18
+ *
19
+ * Checks:
20
+ * 1. First 5 lines must contain a headline-like sentence (bold + icon).
21
+ * 2. Output must contain at least one actionable hint
22
+ * (e.g. `mneme `, `Try `, `Run `).
23
+ * 3. No line wider than 200 visible chars (post-ANSI strip).
24
+ * 4. No section nested deeper than 3 levels (6 leading spaces).
25
+ * 5. Either has `→ Try next` or has fewer than 30 lines (tight).
26
+ */
27
+ export declare function checkContract(output: string): ContractResult;
28
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/iris/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAsCD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAsC5D"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Iris — 30-second contract validator.
3
+ *
4
+ * Every Mneme command output should be scannable in ~30 seconds. This file
5
+ * encodes that as a checkable contract — both for tests ("does my command
6
+ * obey the contract?") and for users (`mneme verify-output`).
7
+ *
8
+ * Pure function. No I/O.
9
+ */
10
+ const ANSI_RE = /\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\/g;
11
+ /** Strip ANSI escapes for visible-width math. Local copy to avoid coupling. */
12
+ export function stripAnsi(s) {
13
+ return s.replace(ANSI_RE, "");
14
+ }
15
+ /** Heuristic: a "headline-like" line has a leading icon glyph + non-trivial
16
+ * text. When ANSI is present we additionally accept a bold escape, but we
17
+ * do NOT require it — pipes and CI strip colour, and the contract has to
18
+ * pass either way. */
19
+ function looksLikeHeadline(line) {
20
+ const visible = stripAnsi(line);
21
+ const head = visible.trimStart();
22
+ if (head.length < 4)
23
+ return false;
24
+ // Non-ASCII glyph (icon) within the first 12 visible chars.
25
+ const hasIcon = /[^\x00-\x7F]/.test(head.slice(0, 12));
26
+ return hasIcon;
27
+ }
28
+ /** Detect whether the output offers an actionable hint. */
29
+ function hasActionableHint(stripped) {
30
+ // "mneme " (a follow-up command), "Try ", "Run ", "→ Try", or fenced cmd.
31
+ if (/\bmneme\s+\w+/.test(stripped))
32
+ return true;
33
+ if (/\bTry\b/.test(stripped))
34
+ return true;
35
+ if (/\bRun\b/.test(stripped))
36
+ return true;
37
+ if (/→\s*Try/.test(stripped))
38
+ return true;
39
+ return false;
40
+ }
41
+ /** Walk lines and find max indent depth (every 2 spaces = 1 level). */
42
+ function maxIndentDepth(lines) {
43
+ let max = 0;
44
+ for (const raw of lines) {
45
+ const visible = stripAnsi(raw);
46
+ const m = visible.match(/^(\s*)/);
47
+ const spaces = m ? m[1].length : 0;
48
+ const depth = Math.floor(spaces / 2);
49
+ if (depth > max)
50
+ max = depth;
51
+ }
52
+ return max;
53
+ }
54
+ /**
55
+ * Validate that a rendered output meets the 30-second readability contract.
56
+ *
57
+ * Checks:
58
+ * 1. First 5 lines must contain a headline-like sentence (bold + icon).
59
+ * 2. Output must contain at least one actionable hint
60
+ * (e.g. `mneme `, `Try `, `Run `).
61
+ * 3. No line wider than 200 visible chars (post-ANSI strip).
62
+ * 4. No section nested deeper than 3 levels (6 leading spaces).
63
+ * 5. Either has `→ Try next` or has fewer than 30 lines (tight).
64
+ */
65
+ export function checkContract(output) {
66
+ const violations = [];
67
+ const rawLines = output.split("\n");
68
+ const stripped = stripAnsi(output);
69
+ // 1. Headline in first 5 lines.
70
+ const top5 = rawLines.slice(0, 5);
71
+ if (!top5.some(looksLikeHeadline)) {
72
+ violations.push("no headline in first 5 lines (need bold + leading icon)");
73
+ }
74
+ // 2. At least one actionable hint.
75
+ if (!hasActionableHint(stripped)) {
76
+ violations.push("no actionable hint found ('mneme', 'Try', 'Run')");
77
+ }
78
+ // 3. No line wider than 200 visible chars.
79
+ for (let i = 0; i < rawLines.length; i++) {
80
+ const visibleLen = stripAnsi(rawLines[i]).length;
81
+ if (visibleLen > 200) {
82
+ violations.push(`line ${i + 1} too wide: ${visibleLen} visible chars (max 200)`);
83
+ break; // one report is enough — keep the message tight
84
+ }
85
+ }
86
+ // 4. Indent depth ≤ 3 levels (i.e. 6 leading spaces).
87
+ const depth = maxIndentDepth(rawLines);
88
+ if (depth > 3) {
89
+ violations.push(`section nested too deep: depth=${depth} (max 3)`);
90
+ }
91
+ // 5. Try-next OR fewer than 30 lines.
92
+ const hasTryNext = /→\s*Try next/.test(stripped) || /\bTry next\b/.test(stripped);
93
+ if (!hasTryNext && rawLines.length >= 30) {
94
+ violations.push(`output is ${rawLines.length} lines without '→ Try next' (cap at 30)`);
95
+ }
96
+ return { ok: violations.length === 0, violations };
97
+ }
98
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/iris/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=contract.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.test.d.ts","sourceRoot":"","sources":["../../src/iris/contract.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { checkContract } from "./contract.js";
3
+ import { renderPyramid, singleSection } from "./pyramid.js";
4
+ beforeEach(() => {
5
+ // Contract checks rely on ANSI bold being present, so we DO want colour on.
6
+ delete process.env.NO_COLOR;
7
+ });
8
+ afterEach(() => {
9
+ process.env.NO_COLOR = "1";
10
+ });
11
+ describe("checkContract — passes on a journalist-shaped output", () => {
12
+ it("a renderPyramid output with headline + actionable hint passes", () => {
13
+ const out = renderPyramid({
14
+ headline: "📰 3 critical anomalies",
15
+ sections: [
16
+ singleSection("lede", "✦ Findings", ["Run mneme forensics --verbose for detail"]),
17
+ ],
18
+ whyShown: "Try forensics next",
19
+ widthOverride: 80,
20
+ });
21
+ const r = checkContract(out);
22
+ if (!r.ok)
23
+ console.error(r.violations, JSON.stringify(out));
24
+ expect(r.ok).toBe(true);
25
+ expect(r.violations).toEqual([]);
26
+ });
27
+ });
28
+ describe("checkContract — failure modes", () => {
29
+ it("flags missing headline (plain text output)", () => {
30
+ const r = checkContract("just a wall of plain text\nwith nothing fancy");
31
+ expect(r.ok).toBe(false);
32
+ expect(r.violations.some((v) => v.includes("headline"))).toBe(true);
33
+ });
34
+ it("flags missing actionable hint", () => {
35
+ // Build output WITH a headline but NO mneme/Try/Run hint.
36
+ const out = "\x1b[1m\x1b[36m 📰 Headline only\x1b[0m\nfiller content\nmore filler\n";
37
+ const r = checkContract(out);
38
+ expect(r.violations.some((v) => v.includes("actionable hint"))).toBe(true);
39
+ });
40
+ it("flags lines wider than 200 chars", () => {
41
+ const huge = "\x1b[1m\x1b[36m 📰 head\x1b[0m\n" + "x".repeat(220) + "\n run mneme forensics";
42
+ const r = checkContract(huge);
43
+ expect(r.violations.some((v) => v.includes("too wide"))).toBe(true);
44
+ });
45
+ it("flags excessively deep indentation", () => {
46
+ const deeplyNested = "\x1b[1m\x1b[36m 📰 ok\x1b[0m\n" +
47
+ " deep content here\n" + // 10 spaces = depth 5
48
+ "Try mneme ask";
49
+ const r = checkContract(deeplyNested);
50
+ expect(r.violations.some((v) => v.includes("nested too deep"))).toBe(true);
51
+ });
52
+ it("flags long output without 'Try next' marker", () => {
53
+ const lines = ["\x1b[1m\x1b[36m 📰 ok\x1b[0m"];
54
+ for (let i = 0; i < 35; i++)
55
+ lines.push(`line ${i}`);
56
+ lines.push("Run mneme ask");
57
+ const r = checkContract(lines.join("\n"));
58
+ expect(r.violations.some((v) => v.includes("Try next"))).toBe(true);
59
+ });
60
+ it("does not flag long output WITH 'Try next'", () => {
61
+ const lines = ["\x1b[1m\x1b[36m 📰 ok\x1b[0m"];
62
+ for (let i = 0; i < 35; i++)
63
+ lines.push(`line ${i}`);
64
+ lines.push("→ Try next: mneme ask");
65
+ const r = checkContract(lines.join("\n"));
66
+ expect(r.violations.some((v) => v.includes("Try next"))).toBe(false);
67
+ });
68
+ });
69
+ describe("checkContract — return shape", () => {
70
+ it("returns ok=true with empty violations on a perfect output", () => {
71
+ const out = renderPyramid({
72
+ headline: "📰 ok",
73
+ sections: [singleSection("body", undefined, ["Run mneme ask now"])],
74
+ widthOverride: 80,
75
+ });
76
+ const r = checkContract(out);
77
+ expect(r).toHaveProperty("ok");
78
+ expect(r).toHaveProperty("violations");
79
+ expect(Array.isArray(r.violations)).toBe(true);
80
+ });
81
+ });
82
+ //# sourceMappingURL=contract.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.test.js","sourceRoot":"","sources":["../../src/iris/contract.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Iris — visual entity continuity.
3
+ *
4
+ * The pillar: "abc1234" must look identical in every command — same colour,
5
+ * same spacing, same prefix glyph — so the user's eye learns one visual
6
+ * language across `mneme ask`, `mneme why`, `mneme forensics`, etc.
7
+ *
8
+ * All renderers below are deterministic: same input → byte-identical output
9
+ * (modulo NO_COLOR for CI). No randomization, no per-command flair.
10
+ *
11
+ * These helpers wrap kleur + osc8. They DO NOT replace `cli/src/ui.ts` —
12
+ * they fix a small set of *visual decisions* (colour, glyph, format) that
13
+ * commands previously made in eight different ways.
14
+ */
15
+ export interface RenderCommitOpts {
16
+ /** Pack onto one line if true (no body indent). Default: false. */
17
+ compact?: boolean;
18
+ /** Highlight as the "primary" commit (e.g. the answer's top citation). */
19
+ emphasized?: boolean;
20
+ /** Click-through URL (GitHub/GitLab/Bitbucket). */
21
+ url?: string;
22
+ }
23
+ export interface RenderableCommit {
24
+ hash: string;
25
+ shortHash?: string;
26
+ subject: string;
27
+ authorName?: string;
28
+ authorDate?: string;
29
+ }
30
+ /**
31
+ * Render a commit citation IDENTICALLY across every command.
32
+ *
33
+ * ● abc1234 [2024-08-12 · alice] feat: add payment retry
34
+ *
35
+ * Compact form:
36
+ * abc1234 feat: add payment retry [alice · 2024-08-12]
37
+ */
38
+ export declare function renderCommit(c: RenderableCommit, opts?: RenderCommitOpts): string;
39
+ export interface RenderAuthorOpts {
40
+ /** Mark this author as "you" (the current git user). */
41
+ isYou?: boolean;
42
+ }
43
+ /**
44
+ * Render an author identity uniformly.
45
+ *
46
+ * alice <alice@bank.com> (default)
47
+ * you (Alice <alice@bank.com>) (isYou)
48
+ */
49
+ export declare function renderAuthor(name: string, email?: string, opts?: RenderAuthorOpts): string;
50
+ export interface RenderFileOpts {
51
+ /** Optional `:start-end` line range. */
52
+ lineRange?: string;
53
+ /** How many times this file was touched (badge). */
54
+ touched?: number;
55
+ }
56
+ /**
57
+ * Render a file path uniformly.
58
+ *
59
+ * src/payment/service.ts
60
+ * src/payment/service.ts:12-44 (with lineRange)
61
+ * src/payment/service.ts (×7) (with touched)
62
+ */
63
+ export declare function renderFile(path: string, opts?: RenderFileOpts): string;
64
+ export interface RenderHashRefOpts {
65
+ /** Click-through URL. */
66
+ url?: string;
67
+ }
68
+ /**
69
+ * Render a commit hash reference INLINE (e.g. inside a sentence).
70
+ *
71
+ * …rolled back in `abc1234`.
72
+ *
73
+ * Always bold + monospace-styled. Never adds a leading dot or trailing meta.
74
+ */
75
+ export declare function renderHashRef(shortHash: string, opts?: RenderHashRefOpts): string;
76
+ //# sourceMappingURL=entity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../../src/iris/entity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA+BH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,GAAE,gBAAqB,GAAG,MAAM,CAkBrF;AAED,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,GAAG,MAAM,CAQ9F;AAED,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,MAAM,CAQ1E;AAED,MAAM,WAAW,iBAAiB;IAChC,yBAAyB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,CAIrF"}