hypomnema 1.0.1 → 1.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.
Files changed (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +12 -5
  4. package/README.md +12 -5
  5. package/commands/audit.md +46 -0
  6. package/commands/crystallize.md +113 -23
  7. package/commands/feedback.md +40 -26
  8. package/commands/ingest.md +31 -9
  9. package/commands/upgrade.md +2 -2
  10. package/docs/ARCHITECTURE.md +83 -9
  11. package/docs/CONTRIBUTING.md +2 -2
  12. package/hooks/hooks.json +39 -1
  13. package/hooks/hypo-auto-commit.mjs +23 -4
  14. package/hooks/hypo-auto-minimal-crystallize.mjs +145 -0
  15. package/hooks/hypo-auto-stage.mjs +9 -5
  16. package/hooks/hypo-compact-guard.mjs +33 -24
  17. package/hooks/hypo-cwd-change.mjs +107 -24
  18. package/hooks/hypo-file-watch.mjs +23 -10
  19. package/hooks/hypo-first-prompt.mjs +37 -23
  20. package/hooks/hypo-hot-rebuild.mjs +31 -8
  21. package/hooks/hypo-lookup.mjs +171 -65
  22. package/hooks/hypo-personal-check.mjs +207 -112
  23. package/hooks/hypo-pre-commit.mjs +46 -0
  24. package/hooks/hypo-session-end.mjs +58 -0
  25. package/hooks/hypo-session-record.mjs +60 -0
  26. package/hooks/hypo-session-start.mjs +312 -44
  27. package/hooks/hypo-shared.mjs +880 -28
  28. package/hooks/hypo-web-fetch-ingest.mjs +121 -0
  29. package/hooks/version-check-fetch.mjs +74 -0
  30. package/hooks/version-check.mjs +184 -0
  31. package/package.json +17 -3
  32. package/scripts/crystallize.mjs +623 -18
  33. package/scripts/doctor.mjs +739 -46
  34. package/scripts/feedback-sync.mjs +974 -0
  35. package/scripts/feedback.mjs +253 -44
  36. package/scripts/graph.mjs +35 -22
  37. package/scripts/ingest.mjs +89 -16
  38. package/scripts/init.mjs +442 -114
  39. package/scripts/lib/design-history-stale.mjs +83 -0
  40. package/scripts/lib/extensions.mjs +749 -0
  41. package/scripts/lib/frontmatter.mjs +5 -1
  42. package/scripts/lib/hypo-ignore.mjs +12 -10
  43. package/scripts/lib/pkg-json.mjs +23 -5
  44. package/scripts/lib/project-create.mjs +225 -0
  45. package/scripts/lib/schema-vocab.mjs +96 -0
  46. package/scripts/lint.mjs +238 -31
  47. package/scripts/query.mjs +26 -10
  48. package/scripts/resume.mjs +11 -5
  49. package/scripts/session-audit.mjs +277 -0
  50. package/scripts/smoke-pack.mjs +224 -0
  51. package/scripts/stats.mjs +24 -10
  52. package/scripts/uninstall.mjs +369 -48
  53. package/scripts/upgrade.mjs +766 -195
  54. package/scripts/verify.mjs +24 -14
  55. package/scripts/weekly-report.mjs +211 -0
  56. package/skills/crystallize/SKILL.md +24 -7
  57. package/skills/graph/SKILL.md +4 -0
  58. package/skills/ingest/SKILL.md +29 -5
  59. package/skills/lint/SKILL.md +4 -0
  60. package/skills/query/SKILL.md +4 -0
  61. package/skills/verify/SKILL.md +4 -0
  62. package/templates/.hypoignore +19 -2
  63. package/templates/Home.md +2 -0
  64. package/templates/SCHEMA.md +61 -6
  65. package/templates/extensions/agents/.gitkeep +0 -0
  66. package/templates/extensions/commands/.gitkeep +0 -0
  67. package/templates/extensions/hooks/.gitkeep +0 -0
  68. package/templates/extensions/skills/.gitkeep +0 -0
  69. package/templates/gitignore +5 -0
  70. package/templates/hot.md +2 -0
  71. package/templates/hypo-config.md +1 -1
  72. package/templates/hypo-guide.md +63 -1
  73. package/templates/hypo-help.md +1 -1
  74. package/templates/pages/observability/_index.md +77 -0
  75. package/templates/projects/_template/index.md +2 -2
  76. package/templates/projects/_template/prd.md +1 -1
@@ -14,8 +14,8 @@ import { join, basename } from 'path';
14
14
  import { HYPO_DIR, buildOutput, loadHypoIgnore, isIgnored } from './hypo-shared.mjs';
15
15
 
16
16
  const INDEX_PATH = join(HYPO_DIR, 'index.md');
17
- const MAX_HITS = 3;
18
- const MAX_CHARS = 2000;
17
+ const MAX_HITS = 3;
18
+ const MAX_CHARS = 2000;
19
19
 
20
20
  // ── helpers ─────────────────────────────────────────────────────────────────
21
21
 
@@ -38,51 +38,120 @@ function buildPageMap(dir, root = dir, map = {}, ignorePatterns = [], hypoDir =
38
38
 
39
39
  function extractKeywords(prompt) {
40
40
  const stop = new Set([
41
- 'the','and','for','with','this','that','have','from','are','was','were',
42
- 'what','when','where','how','why','who','can','could','should','would',
43
- 'does','did','will','not','but','its','also','just','more','any','all',
44
- '어떤','어떻게','무엇','이런','그런','하는','하고','해서','있어','없어',
45
- '되는','이거','저거','그거','이건','그건','저건','같은','하면','되면',
46
- '인지','에서','으로','까지','부터','에게','한테','에도','에만','에는',
41
+ 'the',
42
+ 'and',
43
+ 'for',
44
+ 'with',
45
+ 'this',
46
+ 'that',
47
+ 'have',
48
+ 'from',
49
+ 'are',
50
+ 'was',
51
+ 'were',
52
+ 'what',
53
+ 'when',
54
+ 'where',
55
+ 'how',
56
+ 'why',
57
+ 'who',
58
+ 'can',
59
+ 'could',
60
+ 'should',
61
+ 'would',
62
+ 'does',
63
+ 'did',
64
+ 'will',
65
+ 'not',
66
+ 'but',
67
+ 'its',
68
+ 'also',
69
+ 'just',
70
+ 'more',
71
+ 'any',
72
+ 'all',
73
+ '어떤',
74
+ '어떻게',
75
+ '무엇',
76
+ '이런',
77
+ '그런',
78
+ '하는',
79
+ '하고',
80
+ '해서',
81
+ '있어',
82
+ '없어',
83
+ '되는',
84
+ '이거',
85
+ '저거',
86
+ '그거',
87
+ '이건',
88
+ '그건',
89
+ '저건',
90
+ '같은',
91
+ '하면',
92
+ '되면',
93
+ '인지',
94
+ '에서',
95
+ '으로',
96
+ '까지',
97
+ '부터',
98
+ '에게',
99
+ '한테',
100
+ '에도',
101
+ '에만',
102
+ '에는',
47
103
  ]);
48
- return [...new Set(
49
- prompt.toLowerCase()
50
- .split(/[\s,,.。??!!()\[\]{}'"\/\\:;=+*&%$#@~`|<>]+/)
51
- .filter(w => w.length >= 3 && !stop.has(w))
52
- )];
104
+ return [
105
+ ...new Set(
106
+ prompt
107
+ .toLowerCase()
108
+ .split(/[\s,,.。??!!()\[\]{}'"\/\\:;=+*&%$#@~`|<>]+/)
109
+ .filter((w) => w.length >= 3 && !stop.has(w)),
110
+ ),
111
+ ];
53
112
  }
54
113
 
55
114
  function tokenize(text) {
56
- return text.toLowerCase()
115
+ return text
116
+ .toLowerCase()
57
117
  .split(/[\s\-_/.,,。??!!()\[\]{}'"\\:;=+*&%$#@~`|<>]+/)
58
- .filter(w => w.length >= 2);
118
+ .filter((w) => w.length >= 2);
59
119
  }
60
120
 
61
121
  function bm25Score(queryTerms, entries, k1 = 1.5, b = 0.75) {
62
122
  const N = entries.length;
63
123
  if (N === 0) return [];
64
- const docTokens = entries.map(e => tokenize(e.slug + ' ' + e.desc));
124
+ const docTokens = entries.map((e) => tokenize(e.slug + ' ' + e.desc));
65
125
  const avgdl = docTokens.reduce((s, t) => s + t.length, 0) / N;
66
126
  const df = {};
67
127
  for (const tokens of docTokens) {
68
128
  for (const t of new Set(tokens)) df[t] = (df[t] || 0) + 1;
69
129
  }
70
- const idf = t => Math.log(1 + (N - (df[t] || 0) + 0.5) / ((df[t] || 0) + 0.5));
71
-
72
- return entries.map((e, i) => {
73
- const tokens = docTokens[i];
74
- const dl = tokens.length || 1;
75
- const tf = {};
76
- for (const t of tokens) tf[t] = (tf[t] || 0) + 1;
77
- let score = 0;
78
- for (const q of queryTerms) {
79
- const f = tf[q] || 0;
80
- if (f === 0) continue;
81
- const norm = 1 - b + b * dl / avgdl;
82
- score += idf(q) * (f * (k1 + 1)) / (f + k1 * norm);
83
- }
84
- return { ...e, score };
85
- }).sort((a, c) => c.score - a.score);
130
+ const idf = (t) => Math.log(1 + (N - (df[t] || 0) + 0.5) / ((df[t] || 0) + 0.5));
131
+
132
+ return entries
133
+ .map((e, i) => {
134
+ const tokens = docTokens[i];
135
+ const dl = tokens.length || 1;
136
+ const tf = {};
137
+ for (const t of tokens) tf[t] = (tf[t] || 0) + 1;
138
+ let score = 0;
139
+ for (const q of queryTerms) {
140
+ const f = tf[q] || 0;
141
+ if (f === 0) continue;
142
+ const norm = 1 - b + (b * dl) / avgdl;
143
+ score += (idf(q) * (f * (k1 + 1))) / (f + k1 * norm);
144
+ }
145
+ return { ...e, score };
146
+ })
147
+ .sort((a, c) => c.score - a.score);
148
+ }
149
+
150
+ function typePrior(slug) {
151
+ if (/\/decisions\/|^decisions\//.test(slug)) return 1.5;
152
+ if (/\bprd\b|spec-v/.test(slug)) return 1.3;
153
+ if (/\/session-log\/|\/session-log$/.test(slug)) return 1.2;
154
+ return 1.0;
86
155
  }
87
156
 
88
157
  function parseIndexEntries(indexContent) {
@@ -92,7 +161,7 @@ function parseIndexEntries(indexContent) {
92
161
  if (line.trimStart().startsWith('>')) continue;
93
162
  const m = line.match(/\[\[([^\]]+)\]\]\s*[—\-]+\s*(.+)/);
94
163
  if (!m) continue;
95
- const raw = m[1].trim();
164
+ const raw = m[1].trim();
96
165
  const desc = m[2].trim();
97
166
  const slug = raw.includes('|') ? raw.split('|')[0].trim() : raw;
98
167
  entries.push({ slug, desc });
@@ -104,10 +173,10 @@ function parseIndexEntries(indexContent) {
104
173
 
105
174
  let input = '';
106
175
  process.stdin.setEncoding('utf-8');
107
- process.stdin.on('data', chunk => input += chunk);
176
+ process.stdin.on('data', (chunk) => (input += chunk));
108
177
  process.stdin.on('end', () => {
109
178
  try {
110
- const data = JSON.parse(input);
179
+ const data = JSON.parse(input);
111
180
  const prompt = (data.prompt || '').trim();
112
181
 
113
182
  if (!prompt || !existsSync(INDEX_PATH)) {
@@ -121,58 +190,95 @@ process.stdin.on('end', () => {
121
190
  return;
122
191
  }
123
192
 
124
- const entries = parseIndexEntries(readFileSync(INDEX_PATH, 'utf-8'));
125
- const scored = bm25Score(keywords, entries).filter(e => e.score > 0);
193
+ const entries = parseIndexEntries(readFileSync(INDEX_PATH, 'utf-8'));
194
+ const scored = bm25Score(keywords, entries)
195
+ .map((e) => ({ ...e, score: e.score * typePrior(e.slug) }))
196
+ .sort((a, c) => c.score - a.score)
197
+ .filter((e) => e.score > 0);
126
198
  const topScore = scored[0]?.score ?? 0;
127
- const matched = scored.filter(e => e.score >= topScore * 0.5);
199
+ const matched = scored.filter((e) => e.score >= topScore * 0.5);
128
200
 
129
201
  if (matched.length === 0) {
130
- const topic = keywords.slice(0, 5).join(', ');
131
- const closest = bm25Score(keywords, entries).slice(0, 3).map(e => `[[${e.slug}]]`).join(', ');
132
- console.log(JSON.stringify(
133
- buildOutput(
134
- `[WIKI LOOKUP: miss] "${topic}" — no match. Closest: ${closest || 'none'}`,
135
- { continue: true, suppressOutput: true }
136
- )
137
- ));
202
+ const topic = keywords.slice(0, 5).join(', ');
203
+ const closest = bm25Score(keywords, entries)
204
+ .map((e) => ({ ...e, score: e.score * typePrior(e.slug) }))
205
+ .sort((a, c) => c.score - a.score)
206
+ .slice(0, 3)
207
+ .map((e) => `[[${e.slug}]]`)
208
+ .join(', ');
209
+ console.log(
210
+ JSON.stringify(
211
+ buildOutput(`[WIKI LOOKUP: miss] "${topic}" — no match. Closest: ${closest || 'none'}`, {
212
+ continue: true,
213
+ suppressOutput: true,
214
+ }),
215
+ ),
216
+ );
138
217
  return;
139
218
  }
140
219
 
141
220
  const ignorePatterns = loadHypoIgnore(HYPO_DIR);
142
221
  const pageMap = {
143
- ...buildPageMap(join(HYPO_DIR, 'pages'), join(HYPO_DIR, 'pages'), {}, ignorePatterns, HYPO_DIR),
144
- ...buildPageMap(join(HYPO_DIR, 'projects'), join(HYPO_DIR, 'projects'), {}, ignorePatterns, HYPO_DIR),
222
+ ...buildPageMap(
223
+ join(HYPO_DIR, 'pages'),
224
+ join(HYPO_DIR, 'pages'),
225
+ {},
226
+ ignorePatterns,
227
+ HYPO_DIR,
228
+ ),
229
+ ...buildPageMap(
230
+ join(HYPO_DIR, 'projects'),
231
+ join(HYPO_DIR, 'projects'),
232
+ {},
233
+ ignorePatterns,
234
+ HYPO_DIR,
235
+ ),
145
236
  };
146
237
 
147
238
  const injected = [];
148
239
  for (const { slug } of matched.slice(0, MAX_HITS)) {
149
- const path = pageMap[slug]
150
- ?? pageMap[slug.replace(/^(pages|projects)\//, '')]
151
- ?? pageMap[basename(slug)];
240
+ const path =
241
+ pageMap[slug] ??
242
+ pageMap[slug.replace(/^(pages|projects)\//, '')] ??
243
+ pageMap[basename(slug)];
152
244
  if (path && existsSync(path)) {
153
245
  injected.push(`=== [[${slug}]] ===\n${readFileSync(path, 'utf-8').slice(0, MAX_CHARS)}`);
154
246
  }
155
247
  }
156
248
 
157
249
  if (injected.length === 0) {
158
- const slugs = matched.slice(0, MAX_HITS).map(e => e.slug).join(', ');
159
- console.log(JSON.stringify(
160
- buildOutput(`[WIKI LOOKUP: index hit but files missing] ${slugs}`, { continue: true, suppressOutput: true })
161
- ));
250
+ const slugs = matched
251
+ .slice(0, MAX_HITS)
252
+ .map((e) => e.slug)
253
+ .join(', ');
254
+ console.log(
255
+ JSON.stringify(
256
+ buildOutput(`[WIKI LOOKUP: index hit but files missing] ${slugs}`, {
257
+ continue: true,
258
+ suppressOutput: true,
259
+ }),
260
+ ),
261
+ );
162
262
  return;
163
263
  }
164
264
 
165
- const overflow = matched.length > MAX_HITS
166
- ? `\n(+${matched.length - MAX_HITS} more matches — search wiki index for more)` : '';
265
+ const overflow =
266
+ matched.length > MAX_HITS
267
+ ? `\n(+${matched.length - MAX_HITS} more matches — search wiki index for more)`
268
+ : '';
167
269
 
168
- console.log(JSON.stringify(
169
- buildOutput(
170
- `[WIKI LOOKUP: ${injected.length} page(s) matched]\n\n` + injected.join('\n\n') + overflow,
171
- { continue: true, suppressOutput: true }
172
- )
173
- ));
174
-
175
- } catch {
270
+ console.log(
271
+ JSON.stringify(
272
+ buildOutput(
273
+ `[WIKI LOOKUP: ${injected.length} page(s) matched]\n\n` +
274
+ injected.join('\n\n') +
275
+ overflow,
276
+ { continue: true, suppressOutput: true },
277
+ ),
278
+ ),
279
+ );
280
+ } catch (err) {
281
+ process.stderr.write(`[hypo-lookup] error: ${err?.message ?? String(err)}\n`);
176
282
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
177
283
  }
178
284
  });