@codyswann/lisa 2.236.0 → 2.237.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 (66) hide show
  1. package/dist/cli/check-learnings-budget-cmd.d.ts +20 -0
  2. package/dist/cli/check-learnings-budget-cmd.d.ts.map +1 -0
  3. package/dist/cli/check-learnings-budget-cmd.js +59 -0
  4. package/dist/cli/check-learnings-budget-cmd.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +29 -1
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/core/learnings-budget-check.d.ts +42 -0
  10. package/dist/core/learnings-budget-check.d.ts.map +1 -0
  11. package/dist/core/learnings-budget-check.js +210 -0
  12. package/dist/core/learnings-budget-check.js.map +1 -0
  13. package/dist/core/learnings-document.js +6 -1
  14. package/dist/core/learnings-document.js.map +1 -1
  15. package/package.json +1 -1
  16. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-agy/plugin.json +1 -1
  29. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  49. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-agy/plugin.json +1 -1
  54. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  59. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  64. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  66. package/scripts/check-learnings-budget.ts +29 -163
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.236.0",
3
+ "version": "2.237.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,17 +1,12 @@
1
1
  /** CI gate for the canonical project learnings document and its hard budgets. */
2
- import { constants, existsSync } from "node:fs";
3
- import { open } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
4
3
  import * as path from "node:path";
5
4
  import { pathToFileURL } from "node:url";
6
- import type * as LearningsModule from "@codyswann/lisa/learnings";
5
+ import type * as BudgetCheckModule from "../src/core/learnings-budget-check.js";
7
6
 
8
- type LearningsCheckerModule = Pick<
9
- typeof LearningsModule,
10
- | "LEARNINGS_CONTRACT"
11
- | "estimateLearningTokens"
12
- | "parseLearningsFile"
13
- | "renderLearningsFile"
14
- | "validateLearningEntry"
7
+ type BudgetChecker = Pick<
8
+ typeof BudgetCheckModule,
9
+ "checkLearningsBudget" | "formatDiagnosticPath"
15
10
  >;
16
11
 
17
12
  const DEFAULT_LEARNINGS_FILE = path.resolve(
@@ -36,178 +31,49 @@ async function main(): Promise<void> {
36
31
  ? DEFAULT_LEARNINGS_FILE
37
32
  : path.resolve(process.cwd(), arguments_[0] as string);
38
33
 
39
- try {
40
- const {
41
- LEARNINGS_CONTRACT,
42
- estimateLearningTokens,
43
- parseLearningsFile,
44
- renderLearningsFile,
45
- validateLearningEntry,
46
- } = await loadLearningsModule();
47
- const content = await readBoundedRegularFile(
48
- file,
49
- LEARNINGS_CONTRACT.maxTokens
50
- );
51
- const measuredTokens = estimateLearningTokens(content);
52
- if (measuredTokens > LEARNINGS_CONTRACT.maxTokens) {
53
- throw new Error(
54
- `maxTokens exceeded: measured ${measuredTokens}, allowed ${LEARNINGS_CONTRACT.maxTokens}`
55
- );
56
- }
57
-
58
- const entries = parseLearningsFile(content);
59
- for (const entry of entries) {
60
- validateLearningEntry(entry);
61
- }
62
- if (renderLearningsFile(entries) !== content) {
63
- throw new Error("non-canonical project learnings format");
64
- }
65
-
34
+ const { checkLearningsBudget, formatDiagnosticPath } =
35
+ await loadBudgetChecker();
36
+ const result = await checkLearningsBudget(file);
37
+ if (result.kind === "ok") {
66
38
  console.log(
67
- `${formatDiagnosticPath(file)}: learnings budget passed (${entries.length}/${LEARNINGS_CONTRACT.maxEntries} entries, ${measuredTokens}/${LEARNINGS_CONTRACT.maxTokens} maxTokens)`
68
- );
69
- } catch (error) {
70
- fail(`${formatDiagnosticPath(file)}: ${formatErrorDetail(error)}`);
71
- }
72
- }
73
-
74
- /**
75
- * Render a caught failure without allowing filesystem paths or control bytes
76
- * embedded in an Error message to forge additional terminal/CI output.
77
- * @param error - Unknown thrown failure
78
- * @returns Stable, single-line diagnostic detail
79
- */
80
- function formatErrorDetail(error: unknown): string {
81
- if (error !== null && typeof error === "object") {
82
- const code = readOwnString(error, "code");
83
- if (code !== undefined && /^[A-Z][A-Z0-9_]*$/u.test(code)) {
84
- const syscall = readOwnString(error, "syscall");
85
- return syscall === undefined
86
- ? `filesystem error ${code}`
87
- : `filesystem error ${code} during ${escapeDiagnosticText(syscall)}`;
88
- }
89
- }
90
- const message = error instanceof Error ? error.message : String(error);
91
- return escapeDiagnosticText(message);
92
- }
93
-
94
- /** Read one inert own string property without invoking an accessor. */
95
- function readOwnString(candidate: object, key: string): string | undefined {
96
- const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
97
- return descriptor !== undefined &&
98
- "value" in descriptor &&
99
- typeof descriptor.value === "string"
100
- ? descriptor.value
101
- : undefined;
102
- }
103
-
104
- /** Escape terminal controls while retaining ordinary diagnostic wording. */
105
- function escapeDiagnosticText(value: string): string {
106
- const jsonBody = JSON.stringify(value).slice(1, -1);
107
- return jsonBody.replace(
108
- /[\u007F-\u009F\u2028\u2029]/gu,
109
- character => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
110
- );
111
- }
112
-
113
- /** Quote one path after applying terminal-safe JSON-style escaping. */
114
- function formatDiagnosticPath(file: string): string {
115
- return `"${escapeDiagnosticText(file)}"`;
116
- }
117
-
118
- /**
119
- * Read at most one byte beyond the hard budget from one verified regular-file
120
- * handle. Non-blocking open prevents a FIFO path from stalling the CI gate.
121
- * @param file - Absolute candidate learnings path
122
- * @param maximumBytes - Shared executable maxTokens byte ceiling
123
- * @returns Strictly decoded UTF-8 content within the byte ceiling
124
- */
125
- async function readBoundedRegularFile(
126
- file: string,
127
- maximumBytes: number
128
- ): Promise<string> {
129
- const handle = await open(file, constants.O_RDONLY | constants.O_NONBLOCK);
130
- try {
131
- const before = await handle.stat({ bigint: true });
132
- if (!before.isFile()) {
133
- throw new Error("unsafe input: expected a regular file");
134
- }
135
- if (before.size > BigInt(maximumBytes)) {
136
- throw new Error(
137
- `maxTokens exceeded: measured ${before.size}, allowed ${maximumBytes}`
138
- );
139
- }
140
-
141
- const buffer = Buffer.allocUnsafe(maximumBytes + 1);
142
- let bytesRead = 0;
143
- while (bytesRead < buffer.length) {
144
- const result = await handle.read(
145
- buffer,
146
- bytesRead,
147
- buffer.length - bytesRead,
148
- null
149
- );
150
- if (result.bytesRead === 0) {
151
- break;
152
- }
153
- bytesRead += result.bytesRead;
154
- }
155
- if (bytesRead > maximumBytes) {
156
- throw new Error(
157
- `maxTokens exceeded: measured at least ${bytesRead}, allowed ${maximumBytes}`
158
- );
159
- }
160
-
161
- const after = await handle.stat({ bigint: true });
162
- if (
163
- after.dev !== before.dev ||
164
- after.ino !== before.ino ||
165
- after.size !== before.size ||
166
- after.mtimeNs !== before.mtimeNs ||
167
- after.ctimeNs !== before.ctimeNs
168
- ) {
169
- throw new Error("unsafe input: file changed during bounded read");
170
- }
171
- return new TextDecoder("utf-8", { fatal: true }).decode(
172
- buffer.subarray(0, bytesRead)
39
+ `${formatDiagnosticPath(file)}: learnings budget passed (${result.entryCount}/${result.maxEntries} entries, ${result.measuredTokens}/${result.maxTokens} maxTokens)`
173
40
  );
174
- } finally {
175
- await handle.close();
41
+ return;
176
42
  }
43
+ // This package script targets Lisa's own committed learnings template, which
44
+ // always exists — so a missing file is as much a failure here as any other
45
+ // violation. Host projects use the `lisa check-learnings-budget` CLI
46
+ // subcommand instead, which treats a missing file as an expected pass.
47
+ fail(`${formatDiagnosticPath(file)}: ${result.detail}`);
177
48
  }
178
49
 
179
50
  /**
180
- * Load the executable contract from current source in a checkout or compiled
181
- * output in an npm package. The `.js` source specifier keeps Bun development
182
- * runs aligned with TypeScript's NodeNext resolution while publishing no
183
- * runtime dependency on the excluded `src` tree.
184
- * @returns Canonical executable learnings module
51
+ * Load the reusable budget checker from current source in a checkout or
52
+ * compiled output in an npm package. The `.js` source specifier keeps Bun
53
+ * development runs aligned with TypeScript's NodeNext resolution while
54
+ * publishing no runtime dependency on the excluded `src` tree.
55
+ * @returns Canonical budget-checker functions
185
56
  */
186
- async function loadLearningsModule(): Promise<LearningsCheckerModule> {
57
+ async function loadBudgetChecker(): Promise<BudgetChecker> {
187
58
  const packageRoot = path.resolve(import.meta.dir, "..");
188
59
  const sourceTypescript = path.join(
189
60
  packageRoot,
190
61
  "src",
191
62
  "core",
192
- "learnings.ts"
63
+ "learnings-budget-check.ts"
193
64
  );
194
65
  const runtimeRoot = path.join(
195
66
  packageRoot,
196
67
  existsSync(sourceTypescript) ? "src" : "dist",
197
68
  "core"
198
69
  );
199
- const [contract, document, entry] = await Promise.all([
200
- import(pathToFileURL(path.join(runtimeRoot, "learnings-contract.js")).href),
201
- import(pathToFileURL(path.join(runtimeRoot, "learnings-document.js")).href),
202
- import(pathToFileURL(path.join(runtimeRoot, "learnings-entry.js")).href),
203
- ]);
70
+ const module_ = await import(
71
+ pathToFileURL(path.join(runtimeRoot, "learnings-budget-check.js")).href
72
+ );
204
73
  return {
205
- LEARNINGS_CONTRACT: contract.LEARNINGS_CONTRACT,
206
- estimateLearningTokens: contract.estimateLearningTokens,
207
- parseLearningsFile: document.parseLearningsFile,
208
- renderLearningsFile: document.renderLearningsFile,
209
- validateLearningEntry: entry.validateLearningEntry,
210
- } as LearningsCheckerModule;
74
+ checkLearningsBudget: module_.checkLearningsBudget,
75
+ formatDiagnosticPath: module_.formatDiagnosticPath,
76
+ } as BudgetChecker;
211
77
  }
212
78
 
213
79
  /** Print one deterministic failure diagnostic and exit non-zero. */