thumbgate 1.3.0 → 1.4.1

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 (156) hide show
  1. package/.claude-plugin/README.md +25 -0
  2. package/.claude-plugin/marketplace.json +32 -13
  3. package/.claude-plugin/plugin.json +15 -2
  4. package/.well-known/llms.txt +60 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +242 -126
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/INSTALL.md +59 -4
  9. package/adapters/chatgpt/openapi.yaml +168 -0
  10. package/adapters/claude/.mcp.json +2 -2
  11. package/adapters/codex/config.toml +2 -2
  12. package/adapters/mcp/server-stdio.js +84 -1
  13. package/adapters/opencode/opencode.json +1 -1
  14. package/bin/cli.js +204 -13
  15. package/bin/postinstall.js +8 -2
  16. package/config/budget.json +18 -0
  17. package/config/gates/code-edit.json +61 -0
  18. package/config/gates/db-write.json +61 -0
  19. package/config/gates/default.json +154 -3
  20. package/config/gates/deploy.json +61 -0
  21. package/config/github-about.json +2 -1
  22. package/config/merge-quality-checks.json +23 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -11
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  27. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  28. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  29. package/plugins/codex-profile/.mcp.json +1 -1
  30. package/plugins/codex-profile/INSTALL.md +27 -4
  31. package/plugins/codex-profile/README.md +33 -9
  32. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  33. package/plugins/opencode-profile/INSTALL.md +1 -1
  34. package/public/blog.html +73 -0
  35. package/public/compare/mem0.html +189 -0
  36. package/public/compare/speclock.html +180 -0
  37. package/public/compare.html +10 -2
  38. package/public/guide.html +2 -2
  39. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/codex-cli-guardrails.html +158 -0
  41. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  42. package/public/guides/pre-action-gates.html +162 -0
  43. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  44. package/public/index.html +172 -65
  45. package/public/lessons.html +33 -24
  46. package/public/llm-context.md +140 -0
  47. package/public/pro.html +24 -22
  48. package/scripts/access-anomaly-detector.js +1 -1
  49. package/scripts/adk-consolidator.js +1 -5
  50. package/scripts/agent-security-hardening.js +4 -6
  51. package/scripts/agentic-data-pipeline.js +1 -3
  52. package/scripts/async-job-runner.js +1 -5
  53. package/scripts/audit-trail.js +1 -5
  54. package/scripts/auto-promote-gates.js +5 -3
  55. package/scripts/background-agent-governance.js +2 -10
  56. package/scripts/billing-setup.js +109 -0
  57. package/scripts/billing.js +2 -16
  58. package/scripts/budget-enforcer.js +173 -0
  59. package/scripts/build-claude-mcpb.js +71 -5
  60. package/scripts/build-codex-plugin.js +152 -0
  61. package/scripts/check-congruence.js +132 -14
  62. package/scripts/commercial-offer.js +5 -7
  63. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  64. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  65. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  66. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  67. package/scripts/context-engine.js +21 -6
  68. package/scripts/contextfs.js +1 -21
  69. package/scripts/dashboard.js +20 -0
  70. package/scripts/decision-journal.js +341 -0
  71. package/scripts/delegation-runtime.js +1 -5
  72. package/scripts/distribution-surfaces.js +54 -0
  73. package/scripts/document-intake.js +927 -0
  74. package/scripts/ephemeral-agent-store.js +1 -8
  75. package/scripts/evolution-state.js +1 -5
  76. package/scripts/experiment-tracker.js +1 -5
  77. package/scripts/export-databricks-bundle.js +1 -5
  78. package/scripts/export-hf-dataset.js +1 -5
  79. package/scripts/export-training.js +1 -5
  80. package/scripts/feedback-attribution.js +1 -16
  81. package/scripts/feedback-history-distiller.js +1 -16
  82. package/scripts/feedback-loop.js +1 -5
  83. package/scripts/feedback-root-consolidator.js +2 -21
  84. package/scripts/feedback-session.js +49 -0
  85. package/scripts/feedback-to-rules.js +215 -36
  86. package/scripts/filesystem-search.js +1 -9
  87. package/scripts/fs-utils.js +104 -0
  88. package/scripts/gates-engine.js +200 -11
  89. package/scripts/github-about.js +32 -8
  90. package/scripts/gtm-revenue-loop.js +1 -5
  91. package/scripts/harness-selector.js +148 -0
  92. package/scripts/hosted-config.js +2 -0
  93. package/scripts/hosted-job-launcher.js +1 -5
  94. package/scripts/hybrid-feedback-context.js +33 -49
  95. package/scripts/intervention-policy.js +58 -1
  96. package/scripts/lesson-db.js +3 -18
  97. package/scripts/lesson-inference.js +194 -16
  98. package/scripts/lesson-retrieval.js +60 -24
  99. package/scripts/llm-client.js +59 -0
  100. package/scripts/managed-lesson-agent.js +183 -0
  101. package/scripts/marketing-experiment.js +8 -22
  102. package/scripts/meta-agent-loop.js +624 -0
  103. package/scripts/metered-billing.js +1 -1
  104. package/scripts/money-watcher.js +1 -4
  105. package/scripts/obsidian-export.js +1 -5
  106. package/scripts/operational-integrity.js +15 -3
  107. package/scripts/operational-summary.js +41 -5
  108. package/scripts/org-dashboard.js +6 -1
  109. package/scripts/per-step-scoring.js +2 -4
  110. package/scripts/pr-manager.js +201 -19
  111. package/scripts/pro-features.js +3 -2
  112. package/scripts/prompt-dlp.js +3 -3
  113. package/scripts/prove-adapters.js +1 -5
  114. package/scripts/prove-attribution.js +1 -5
  115. package/scripts/prove-automation.js +1 -3
  116. package/scripts/prove-cloudflare-sandbox.js +1 -3
  117. package/scripts/prove-data-pipeline.js +1 -3
  118. package/scripts/prove-intelligence.js +1 -3
  119. package/scripts/prove-lancedb.js +1 -5
  120. package/scripts/prove-local-intelligence.js +1 -3
  121. package/scripts/prove-packaged-runtime.js +75 -9
  122. package/scripts/prove-predictive-insights.js +1 -3
  123. package/scripts/prove-training-export.js +1 -3
  124. package/scripts/prove-workflow-contract.js +1 -5
  125. package/scripts/ralph-loop.js +376 -0
  126. package/scripts/ralph-mode-ci.js +331 -0
  127. package/scripts/rate-limiter.js +3 -1
  128. package/scripts/reddit-dm-outreach.js +14 -4
  129. package/scripts/rotate-stripe-webhook-secret.js +314 -0
  130. package/scripts/schedule-manager.js +3 -5
  131. package/scripts/security-scanner.js +448 -0
  132. package/scripts/self-distill-agent.js +579 -0
  133. package/scripts/semantic-dedup.js +115 -0
  134. package/scripts/skill-exporter.js +1 -3
  135. package/scripts/skill-generator.js +1 -5
  136. package/scripts/social-analytics/engagement-audit.js +1 -18
  137. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  138. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  139. package/scripts/social-analytics/publishers/zernio.js +51 -0
  140. package/scripts/social-pipeline.js +1 -3
  141. package/scripts/social-post-hourly.js +47 -4
  142. package/scripts/statusline-links.js +6 -5
  143. package/scripts/statusline.sh +29 -153
  144. package/scripts/sync-branch-protection.js +340 -0
  145. package/scripts/tessl-export.js +1 -3
  146. package/scripts/thumbgate-search.js +32 -1
  147. package/scripts/tool-kpi-tracker.js +1 -1
  148. package/scripts/tool-registry.js +106 -2
  149. package/scripts/vector-store.js +1 -5
  150. package/scripts/weekly-auto-post.js +1 -1
  151. package/scripts/workflow-sentinel.js +91 -0
  152. package/skills/thumbgate/SKILL.md +1 -1
  153. package/src/api/server.js +296 -7
  154. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  155. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  156. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -0,0 +1,927 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+
7
+ const { getFeedbackPaths } = require('./feedback-loop');
8
+ const { loadGateTemplates } = require('./gate-templates');
9
+
10
+ const DOCUMENTS_DIRNAME = 'documents';
11
+ const DOCUMENT_CATALOG_FILENAME = 'catalog.jsonl';
12
+ const DOCUMENT_FILE_SUFFIX = '.json';
13
+ const MAX_POLICY_PROPOSALS = 8;
14
+ const MAX_SEARCH_SCAN = 200;
15
+
16
+ const TEXT_FORMAT_ALIASES = {
17
+ '.md': 'markdown',
18
+ '.markdown': 'markdown',
19
+ '.txt': 'text',
20
+ '.text': 'text',
21
+ '.rst': 'text',
22
+ '.adoc': 'text',
23
+ '.csv': 'text',
24
+ '.log': 'text',
25
+ '.yaml': 'yaml',
26
+ '.yml': 'yaml',
27
+ '.json': 'json',
28
+ '.html': 'html',
29
+ '.htm': 'html',
30
+ };
31
+
32
+ const POLICY_LINE_PATTERNS = [
33
+ /\bmust\b/i,
34
+ /\bmust\s+not\b/i,
35
+ /\bshould\b/i,
36
+ /\bshould\s+not\b/i,
37
+ /\bdo not\b/i,
38
+ /\bdon't\b/i,
39
+ /\bnever\b/i,
40
+ /\balways\b/i,
41
+ /\brequired?\b/i,
42
+ /\bforbid(?:den)?\b/i,
43
+ /\bonly\b/i,
44
+ /\bblock(?:ed)?\b/i,
45
+ /\bdeny\b/i,
46
+ /\bapproved?\b/i,
47
+ /\bverify\b/i,
48
+ /\bverification\b/i,
49
+ /\bproof\b/i,
50
+ ];
51
+ const HIGH_SEVERITY_PATTERNS = [
52
+ /\bproduction\b/i,
53
+ /\bprod\b/i,
54
+ /\bmain\b/i,
55
+ /\bmaster\b/i,
56
+ /\bforce(?:\s|-)?push\b/i,
57
+ /\bdrop\b/i,
58
+ /\btruncate\b/i,
59
+ /\bdelete\b/i,
60
+ /\bsecret\b/i,
61
+ /\btoken\b/i,
62
+ /\bcredential\b/i,
63
+ /\bapi[_ -]?key\b/i,
64
+ /\bpublish\b/i,
65
+ /\brelease\b/i,
66
+ ];
67
+ const MEDIUM_SEVERITY_PATTERNS = [
68
+ /\btests?\b/i,
69
+ /\bverify\b/i,
70
+ /\bverification\b/i,
71
+ /\bproof\b/i,
72
+ /\breview\b/i,
73
+ /\bci\b/i,
74
+ /\blint\b/i,
75
+ /\bbranch\b/i,
76
+ /\bworkflow\b/i,
77
+ /\bdeploy\b/i,
78
+ ];
79
+ const BLOCK_ACTION_PATTERNS = [
80
+ /\bnever\b/i,
81
+ /\bmust not\b/i,
82
+ /\bdo not\b/i,
83
+ /\bdon't\b/i,
84
+ /\bforbid(?:den)?\b/i,
85
+ /\bblock(?:ed)?\b/i,
86
+ /\bdeny\b/i,
87
+ ];
88
+ const WARN_ACTION_PATTERNS = [
89
+ /\balways\b/i,
90
+ /\brequired?\b/i,
91
+ /\bverify\b/i,
92
+ /\bverification\b/i,
93
+ /\bproof\b/i,
94
+ /\breview\b/i,
95
+ ];
96
+
97
+ const TEMPLATE_HINTS = {
98
+ 'never-force-push-main': [
99
+ /force(?:\s|-)?push/i,
100
+ /git\s+push\s+(?:--force|-f)/i,
101
+ /protected branch/i,
102
+ ],
103
+ 'never-skip-tests-before-commit': [
104
+ /skip\s+tests?/i,
105
+ /before\s+commit/i,
106
+ /run\s+(?:the\s+)?tests?/i,
107
+ /\bci\b/i,
108
+ /\blint\b/i,
109
+ ],
110
+ 'evidence-before-done': [
111
+ /\b(?:evidence|proof)\b/i,
112
+ /\bverified?\b/i,
113
+ /\bdone\b/i,
114
+ /claim(?:ing)?\s+success/i,
115
+ ],
116
+ 'protect-production-sql': [
117
+ /\b(?:drop|truncate|delete)\b/i,
118
+ /\b(?:production|prod)\b/i,
119
+ /\b(?:sql|database|db|table|tables)\b/i,
120
+ ],
121
+ 'back-up-env-before-edit': [
122
+ /\.env\b/i,
123
+ /\b(?:backup|back up|copy)\b/i,
124
+ /\b(?:secret|token|credential)\b/i,
125
+ ],
126
+ 'promote-known-good-workflows': [
127
+ /\bknown[-\s]?good\b/i,
128
+ /\brecommended workflow\b/i,
129
+ /\bgolden path\b/i,
130
+ /\bbest practice(?:s)?\b/i,
131
+ ],
132
+ };
133
+
134
+ function nowIso() {
135
+ return new Date().toISOString();
136
+ }
137
+
138
+ function ensureDir(dirPath) {
139
+ if (!fs.existsSync(dirPath)) {
140
+ fs.mkdirSync(dirPath, { recursive: true });
141
+ }
142
+ }
143
+
144
+ function readJsonl(filePath) {
145
+ if (!fs.existsSync(filePath)) return [];
146
+ return fs.readFileSync(filePath, 'utf8')
147
+ .split('\n')
148
+ .map((line) => line.trim())
149
+ .filter(Boolean)
150
+ .map((line) => {
151
+ try {
152
+ return JSON.parse(line);
153
+ } catch {
154
+ return null;
155
+ }
156
+ })
157
+ .filter(Boolean);
158
+ }
159
+
160
+ function writeJson(filePath, payload) {
161
+ ensureDir(path.dirname(filePath));
162
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
163
+ }
164
+
165
+ function writeJsonl(filePath, records) {
166
+ ensureDir(path.dirname(filePath));
167
+ const body = records.map((record) => JSON.stringify(record)).join('\n');
168
+ fs.writeFileSync(filePath, body ? `${body}\n` : '', 'utf8');
169
+ }
170
+
171
+ function normalizeText(value) {
172
+ const withoutBom = String(value || '').split('\uFEFF').join('');
173
+ const normalizedNewlines = normalizeNewlines(withoutBom);
174
+ const trimmedLines = normalizedNewlines
175
+ .split('\n')
176
+ .map(trimTrailingSpacesAndTabs)
177
+ .join('\n');
178
+ return collapseBlankLines(trimmedLines).trim();
179
+ }
180
+
181
+ function safeArray(values) {
182
+ return Array.isArray(values) ? values : [];
183
+ }
184
+
185
+ function normalizeTags(tags) {
186
+ if (!Array.isArray(tags)) return [];
187
+ return Array.from(new Set(tags
188
+ .map((tag) => String(tag || '').trim())
189
+ .filter(Boolean)));
190
+ }
191
+
192
+ function normalizeNewlines(value) {
193
+ let result = '';
194
+ const text = String(value || '');
195
+ for (let index = 0; index < text.length; index += 1) {
196
+ const char = text[index];
197
+ if (char !== '\r') {
198
+ result += char;
199
+ continue;
200
+ }
201
+ result += '\n';
202
+ if (text[index + 1] === '\n') {
203
+ index += 1;
204
+ }
205
+ }
206
+ return result;
207
+ }
208
+
209
+ function trimTrailingSpacesAndTabs(value) {
210
+ const text = String(value || '');
211
+ let end = text.length;
212
+ while (end > 0 && (text[end - 1] === ' ' || text[end - 1] === '\t')) {
213
+ end -= 1;
214
+ }
215
+ return text.slice(0, end);
216
+ }
217
+
218
+ function collapseBlankLines(value) {
219
+ const compacted = [];
220
+ let blankCount = 0;
221
+ for (const line of String(value || '').split('\n')) {
222
+ if (line === '') {
223
+ blankCount += 1;
224
+ if (blankCount <= 2) compacted.push(line);
225
+ continue;
226
+ }
227
+ blankCount = 0;
228
+ compacted.push(line);
229
+ }
230
+ return compacted.join('\n');
231
+ }
232
+
233
+ function slugify(value) {
234
+ const output = [];
235
+ let previousWasDash = false;
236
+ for (const char of String(value || '').toLowerCase()) {
237
+ const code = char.charCodeAt(0);
238
+ const isAlphanumeric = (code >= 97 && code <= 122) || (code >= 48 && code <= 57);
239
+ if (isAlphanumeric) {
240
+ output.push(char);
241
+ previousWasDash = false;
242
+ continue;
243
+ }
244
+ if (!previousWasDash && output.length > 0) {
245
+ output.push('-');
246
+ previousWasDash = true;
247
+ }
248
+ }
249
+ if (output[output.length - 1] === '-') {
250
+ output.pop();
251
+ }
252
+ return output.join('');
253
+ }
254
+
255
+ function sha256(value) {
256
+ return crypto.createHash('sha256').update(String(value || ''), 'utf8').digest('hex');
257
+ }
258
+
259
+ function matchesAnyPattern(value, patterns) {
260
+ return patterns.some((pattern) => pattern.test(value));
261
+ }
262
+
263
+ function decodeHtmlEntities(text) {
264
+ const entityMap = {
265
+ '&amp;': '&',
266
+ '&lt;': '<',
267
+ '&gt;': '>',
268
+ '&quot;': '"',
269
+ '&#39;': "'",
270
+ '&nbsp;': ' ',
271
+ };
272
+
273
+ return String(text || '').replaceAll(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (match) => entityMap[match] || match);
274
+ }
275
+
276
+ function stripElementBlocks(html, tagName) {
277
+ let remaining = String(html || '');
278
+ let lower = remaining.toLowerCase();
279
+ const openToken = `<${tagName}`;
280
+ const closeToken = `</${tagName}`;
281
+ let result = '';
282
+
283
+ while (remaining) {
284
+ const openIndex = lower.indexOf(openToken);
285
+ if (openIndex === -1) {
286
+ result += remaining;
287
+ break;
288
+ }
289
+
290
+ result += remaining.slice(0, openIndex);
291
+ const closeIndex = lower.indexOf(closeToken, openIndex + openToken.length);
292
+ if (closeIndex === -1) break;
293
+
294
+ const closeEnd = remaining.indexOf('>', closeIndex + closeToken.length);
295
+ if (closeEnd === -1) break;
296
+
297
+ remaining = remaining.slice(closeEnd + 1);
298
+ lower = remaining.toLowerCase();
299
+ }
300
+
301
+ return result;
302
+ }
303
+
304
+ function stripHtmlComments(html) {
305
+ let remaining = String(html || '');
306
+ let result = '';
307
+
308
+ while (remaining) {
309
+ const start = remaining.indexOf('<!--');
310
+ if (start === -1) {
311
+ result += remaining;
312
+ break;
313
+ }
314
+
315
+ result += remaining.slice(0, start);
316
+ const end = remaining.indexOf('-->', start + 4);
317
+ if (end === -1) break;
318
+ remaining = remaining.slice(end + 3);
319
+ }
320
+
321
+ return result;
322
+ }
323
+
324
+ function getTagName(tagContent) {
325
+ const trimmed = String(tagContent || '').trim();
326
+ let start = 0;
327
+ if (trimmed[start] === '/') start += 1;
328
+
329
+ let end = start;
330
+ while (end < trimmed.length) {
331
+ const char = trimmed[end];
332
+ const code = char.charCodeAt(0);
333
+ const isNameChar = (code >= 97 && code <= 122)
334
+ || (code >= 65 && code <= 90)
335
+ || (code >= 48 && code <= 57)
336
+ || char === '-';
337
+ if (!isNameChar) break;
338
+ end += 1;
339
+ }
340
+
341
+ return trimmed.slice(start, end).toLowerCase();
342
+ }
343
+
344
+ function htmlToText(html) {
345
+ const blockTags = new Set([
346
+ 'p', 'div', 'section', 'article', 'header', 'footer', 'aside', 'main',
347
+ 'li', 'tr', 'td', 'th', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br',
348
+ ]);
349
+ const withoutScripts = stripElementBlocks(html, 'script');
350
+ const withoutStyles = stripElementBlocks(withoutScripts, 'style');
351
+ const text = stripHtmlComments(withoutStyles);
352
+ let result = '';
353
+ let cursor = 0;
354
+
355
+ while (cursor < text.length) {
356
+ const tagStart = text.indexOf('<', cursor);
357
+ if (tagStart === -1) {
358
+ result += text.slice(cursor);
359
+ break;
360
+ }
361
+
362
+ result += text.slice(cursor, tagStart);
363
+ const tagEnd = text.indexOf('>', tagStart + 1);
364
+ if (tagEnd === -1) {
365
+ result += ' ';
366
+ break;
367
+ }
368
+
369
+ const tagName = getTagName(text.slice(tagStart + 1, tagEnd));
370
+ result += blockTags.has(tagName) ? '\n' : ' ';
371
+ cursor = tagEnd + 1;
372
+ }
373
+
374
+ return result;
375
+ }
376
+
377
+ /**
378
+ * Strip HTML tags and dangerous content from a string.
379
+ * This function is used for text extraction only — output is never rendered as HTML.
380
+ * Defense-in-depth: strips scripts, styles, event handlers, and all remaining tags.
381
+ */
382
+ function stripHtml(html) {
383
+ return normalizeText(decodeHtmlEntities(htmlToText(html)));
384
+ }
385
+
386
+ function inferSourceFormat(filePath, explicitFormat) {
387
+ if (explicitFormat) {
388
+ return String(explicitFormat).trim().toLowerCase();
389
+ }
390
+
391
+ const ext = path.extname(String(filePath || '')).toLowerCase();
392
+ return TEXT_FORMAT_ALIASES[ext] || null;
393
+ }
394
+
395
+ function normalizeDocumentBody(rawContent, sourceFormat) {
396
+ const normalizedFormat = String(sourceFormat || '').trim().toLowerCase();
397
+ const rawText = String(rawContent || '');
398
+ if (!rawText.trim()) {
399
+ throw new Error('document content is empty');
400
+ }
401
+
402
+ if (normalizedFormat === 'html') {
403
+ return stripHtml(rawText);
404
+ }
405
+
406
+ if (normalizedFormat === 'json') {
407
+ try {
408
+ const parsed = JSON.parse(rawText);
409
+ return normalizeText(JSON.stringify(parsed, null, 2));
410
+ } catch {
411
+ return normalizeText(rawText);
412
+ }
413
+ }
414
+
415
+ if (['markdown', 'text', 'yaml'].includes(normalizedFormat)) {
416
+ return normalizeText(rawText);
417
+ }
418
+
419
+ throw new Error(`Unsupported document format: ${normalizedFormat || 'unknown'}`);
420
+ }
421
+
422
+ function extractMarkdownTitle(normalizedContent) {
423
+ for (const line of String(normalizedContent || '').split('\n')) {
424
+ if (!line.startsWith('# ')) continue;
425
+ const title = line.slice(2).trim();
426
+ if (title) return title;
427
+ }
428
+ return null;
429
+ }
430
+
431
+ function extractHtmlTitle(rawContent) {
432
+ const html = String(rawContent || '');
433
+ const lower = html.toLowerCase();
434
+ const openStart = lower.indexOf('<title');
435
+ if (openStart === -1) return null;
436
+ const openEnd = html.indexOf('>', openStart + 6);
437
+ if (openEnd === -1) return null;
438
+ const closeStart = lower.indexOf('</title', openEnd + 1);
439
+ if (closeStart === -1) return null;
440
+ const title = decodeHtmlEntities(html.slice(openEnd + 1, closeStart)).trim();
441
+ return title || null;
442
+ }
443
+
444
+ function extractJsonTitle(rawContent) {
445
+ try {
446
+ const parsed = JSON.parse(String(rawContent || ''));
447
+ if (!parsed || typeof parsed !== 'object') return null;
448
+ for (const key of ['title', 'name', 'policy', 'document']) {
449
+ const value = parsed[key];
450
+ if (typeof value === 'string' && value.trim()) {
451
+ return value.trim();
452
+ }
453
+ }
454
+ } catch {
455
+ return null;
456
+ }
457
+ return null;
458
+ }
459
+
460
+ function extractFallbackTitle(filePath) {
461
+ return filePath
462
+ ? path.basename(filePath, path.extname(filePath))
463
+ : 'Imported document';
464
+ }
465
+
466
+ function extractFormatTitle(sourceFormat, rawContent) {
467
+ if (sourceFormat === 'html') {
468
+ return extractHtmlTitle(rawContent);
469
+ }
470
+ if (sourceFormat === 'json') {
471
+ return extractJsonTitle(rawContent);
472
+ }
473
+ return null;
474
+ }
475
+
476
+ function extractTitle({ explicitTitle, filePath, rawContent, normalizedContent, sourceFormat }) {
477
+ const provided = String(explicitTitle || '').trim();
478
+ if (provided) return provided;
479
+
480
+ return extractMarkdownTitle(normalizedContent)
481
+ || extractFormatTitle(sourceFormat, rawContent)
482
+ || extractFallbackTitle(filePath);
483
+ }
484
+
485
+ function extractHeadings(content) {
486
+ return String(content || '')
487
+ .split('\n')
488
+ .map(extractMarkdownHeading)
489
+ .filter(Boolean)
490
+ .slice(0, 12);
491
+ }
492
+
493
+ function extractMarkdownHeading(line) {
494
+ const text = String(line || '');
495
+ let level = 0;
496
+ while (level < text.length && text[level] === '#' && level < 6) {
497
+ level += 1;
498
+ }
499
+ if (level === 0 || text[level] !== ' ') return null;
500
+ const heading = text.slice(level + 1).trim();
501
+ return heading || null;
502
+ }
503
+
504
+ function buildExcerpt(content, maxLength = 280) {
505
+ const compact = String(content || '').replaceAll(/\s+/g, ' ').trim();
506
+ if (!compact) return '';
507
+ if (compact.length <= maxLength) return compact;
508
+ return `${compact.slice(0, maxLength - 1)}\u2026`;
509
+ }
510
+
511
+ function normalizePolicyLine(line) {
512
+ return String(line || '')
513
+ .replaceAll(/^#{1,6}\s+/g, '')
514
+ .replaceAll(/^[-*+]\s+/g, '')
515
+ .replaceAll(/^\d+\.\s+/g, '')
516
+ .replaceAll(/\s+/g, ' ')
517
+ .trim();
518
+ }
519
+
520
+ function uniqueBy(items, selector) {
521
+ const seen = new Set();
522
+ const results = [];
523
+ for (const item of items) {
524
+ const key = selector(item);
525
+ if (!key || seen.has(key)) continue;
526
+ seen.add(key);
527
+ results.push(item);
528
+ }
529
+ return results;
530
+ }
531
+
532
+ function extractPolicyStatements(content) {
533
+ const lines = String(content || '')
534
+ .split('\n')
535
+ .map(normalizePolicyLine)
536
+ .filter(Boolean)
537
+ .filter((line) => line.length >= 18 && line.length <= 220)
538
+ .filter((line) => matchesAnyPattern(line, POLICY_LINE_PATTERNS));
539
+
540
+ return uniqueBy(lines, (line) => line.toLowerCase()).slice(0, MAX_POLICY_PROPOSALS * 2);
541
+ }
542
+
543
+ function inferProposalSeverity(statement) {
544
+ if (matchesAnyPattern(statement, HIGH_SEVERITY_PATTERNS)) return 'critical';
545
+ if (matchesAnyPattern(statement, MEDIUM_SEVERITY_PATTERNS)) return 'high';
546
+ return 'medium';
547
+ }
548
+
549
+ function inferProposalAction(statement) {
550
+ if (matchesAnyPattern(statement, BLOCK_ACTION_PATTERNS)) {
551
+ return 'block';
552
+ }
553
+ if (matchesAnyPattern(statement, WARN_ACTION_PATTERNS)) {
554
+ return 'warn';
555
+ }
556
+ return 'warn';
557
+ }
558
+
559
+ function tokenize(value) {
560
+ return Array.from(new Set(
561
+ String(value || '')
562
+ .toLowerCase()
563
+ .match(/[a-z0-9_.-]{3,}/g) || []
564
+ ));
565
+ }
566
+
567
+ function countMatches(text, token) {
568
+ const haystack = String(text || '').toLowerCase();
569
+ if (!token || !haystack) return 0;
570
+
571
+ let count = 0;
572
+ let cursor = 0;
573
+ while (count < 5) {
574
+ const index = haystack.indexOf(token, cursor);
575
+ if (index === -1) break;
576
+ count += 1;
577
+ cursor = index + token.length;
578
+ }
579
+ return count;
580
+ }
581
+
582
+ function scoreTemplateAgainstText(template, text) {
583
+ const matchers = TEMPLATE_HINTS[template.id] || [];
584
+ const hitCount = matchers.reduce((sum, matcher) => sum + (matcher.test(text) ? 1 : 0), 0);
585
+
586
+ if (template.id === 'protect-production-sql') {
587
+ return hitCount >= 2 ? hitCount : 0;
588
+ }
589
+ if (template.id === 'evidence-before-done') {
590
+ return hitCount >= 2 ? hitCount : 0;
591
+ }
592
+ if (template.id === 'back-up-env-before-edit') {
593
+ return hitCount >= 1 ? hitCount : 0;
594
+ }
595
+ return hitCount >= 1 ? hitCount : 0;
596
+ }
597
+
598
+ function findSupportingExcerpt(content, templateId) {
599
+ const statements = extractPolicyStatements(content);
600
+ const matchers = TEMPLATE_HINTS[templateId] || [];
601
+ const statement = statements.find((line) => matchers.some((matcher) => matcher.test(line)));
602
+ if (statement) return statement;
603
+ return buildExcerpt(content, 220);
604
+ }
605
+
606
+ function buildTemplateProposal(document, template, score) {
607
+ const evidence = findSupportingExcerpt(document.content, template.id);
608
+ const proposalId = `proposal_${template.id}_${document.documentId.slice(-8)}`;
609
+ return {
610
+ proposalId,
611
+ type: 'gate_template',
612
+ status: 'proposed',
613
+ title: template.name,
614
+ templateId: template.id,
615
+ sourceDocumentId: document.documentId,
616
+ action: template.defaultAction,
617
+ severity: template.severity,
618
+ score,
619
+ evidence,
620
+ rationale: template.problem,
621
+ roi: template.roi,
622
+ rollout: template.rollout,
623
+ readyToActivate: true,
624
+ recommendedConfig: {
625
+ id: `${template.id}-${document.documentId.slice(-8)}`,
626
+ action: template.defaultAction,
627
+ severity: template.severity,
628
+ pattern: template.pattern,
629
+ message: `Imported policy "${document.title}" recommends: ${template.name}.`,
630
+ },
631
+ };
632
+ }
633
+
634
+ function buildPolicyProposal(document, statement) {
635
+ const proposalInput = `${document.documentId}:${statement}`;
636
+ const proposalId = `proposal_${sha256(proposalInput).slice(0, 12)}`;
637
+ const severity = inferProposalSeverity(statement);
638
+ const action = inferProposalAction(statement);
639
+ return {
640
+ proposalId,
641
+ type: 'policy_statement',
642
+ status: 'proposed',
643
+ title: statement.length > 96 ? `${statement.slice(0, 95)}\u2026` : statement,
644
+ templateId: null,
645
+ sourceDocumentId: document.documentId,
646
+ action,
647
+ severity,
648
+ score: 1,
649
+ evidence: statement,
650
+ rationale: `Imported from policy document "${document.title}"`,
651
+ roi: 'Converts human policy language into a reviewable ThumbGate gate candidate.',
652
+ rollout: 'Review and tailor the command pattern before activation.',
653
+ readyToActivate: false,
654
+ recommendedConfig: {
655
+ action,
656
+ severity,
657
+ message: statement,
658
+ reviewRequired: true,
659
+ },
660
+ };
661
+ }
662
+
663
+ function proposeGatesFromDocument(document, options = {}) {
664
+ const maxProposals = Number.isFinite(Number(options.maxProposals))
665
+ ? Math.max(1, Math.min(12, Number(options.maxProposals)))
666
+ : MAX_POLICY_PROPOSALS;
667
+ const templates = safeArray(loadGateTemplates().templates);
668
+ const templateProposals = templates
669
+ .map((template) => ({
670
+ template,
671
+ score: scoreTemplateAgainstText(template, document.content),
672
+ }))
673
+ .filter((entry) => entry.score > 0)
674
+ .sort((left, right) => right.score - left.score)
675
+ .map((entry) => buildTemplateProposal(document, entry.template, entry.score));
676
+
677
+ const consumedStatements = new Set(templateProposals.map((proposal) => proposal.evidence.toLowerCase()));
678
+ const policyProposals = extractPolicyStatements(document.content)
679
+ .filter((statement) => !consumedStatements.has(statement.toLowerCase()))
680
+ .map((statement) => buildPolicyProposal(document, statement));
681
+
682
+ return uniqueBy([
683
+ ...templateProposals,
684
+ ...policyProposals,
685
+ ], (proposal) => proposal.proposalId).slice(0, maxProposals);
686
+ }
687
+
688
+ function getDocumentStorePaths(options = {}) {
689
+ const feedbackDir = options.feedbackDir || getFeedbackPaths().FEEDBACK_DIR;
690
+ const documentsDir = path.join(feedbackDir, DOCUMENTS_DIRNAME);
691
+ return {
692
+ feedbackDir,
693
+ documentsDir,
694
+ catalogPath: path.join(documentsDir, DOCUMENT_CATALOG_FILENAME),
695
+ };
696
+ }
697
+
698
+ function getDocumentPath(documentId, options = {}) {
699
+ const { documentsDir } = getDocumentStorePaths(options);
700
+ return path.join(documentsDir, `${documentId}${DOCUMENT_FILE_SUFFIX}`);
701
+ }
702
+
703
+ function buildDocumentSummary(document) {
704
+ return {
705
+ documentId: document.documentId,
706
+ title: document.title,
707
+ sourceType: document.sourceType,
708
+ sourcePath: document.sourcePath || null,
709
+ sourceName: document.sourceName || null,
710
+ sourceFormat: document.sourceFormat,
711
+ importedAt: document.importedAt,
712
+ tags: normalizeTags(document.tags),
713
+ excerpt: document.excerpt,
714
+ lineCount: document.lineCount,
715
+ headingCount: safeArray(document.headings).length,
716
+ proposalCount: safeArray(document.proposals).length,
717
+ matchedTemplateIds: safeArray(document.matchedTemplateIds),
718
+ fingerprint: document.fingerprint,
719
+ };
720
+ }
721
+
722
+ function readImportedDocument(documentId, options = {}) {
723
+ const filePath = getDocumentPath(String(documentId || '').trim(), options);
724
+ if (!fs.existsSync(filePath)) return null;
725
+ try {
726
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
727
+ } catch {
728
+ return null;
729
+ }
730
+ }
731
+
732
+ function listImportedDocuments(options = {}) {
733
+ const limit = Number.isFinite(Number(options.limit))
734
+ ? Math.max(1, Math.min(MAX_SEARCH_SCAN, Number(options.limit)))
735
+ : 20;
736
+ const query = String(options.query || '').trim().toLowerCase();
737
+ const requestedTag = String(options.tag || '').trim().toLowerCase();
738
+ const { catalogPath } = getDocumentStorePaths(options);
739
+ const documents = readJsonl(catalogPath);
740
+
741
+ const filtered = documents.filter((document) => {
742
+ const tags = safeArray(document.tags).map((tag) => String(tag).toLowerCase());
743
+ const matchedTemplateIds = safeArray(document.matchedTemplateIds).map((tag) => String(tag).toLowerCase());
744
+ if (requestedTag && !tags.includes(requestedTag) && !matchedTemplateIds.includes(requestedTag)) {
745
+ return false;
746
+ }
747
+ if (!query) return true;
748
+ const haystack = [
749
+ document.title,
750
+ document.excerpt,
751
+ tags.join(' '),
752
+ matchedTemplateIds.join(' '),
753
+ ].join(' ').toLowerCase();
754
+ return haystack.includes(query);
755
+ });
756
+
757
+ return {
758
+ total: filtered.length,
759
+ returned: filtered.slice(0, limit).length,
760
+ documents: filtered.slice(0, limit),
761
+ };
762
+ }
763
+
764
+ function persistDocument(document, options = {}) {
765
+ const paths = getDocumentStorePaths(options);
766
+ ensureDir(paths.documentsDir);
767
+ writeJson(getDocumentPath(document.documentId, options), document);
768
+ const summaries = listImportedDocuments({
769
+ ...options,
770
+ limit: MAX_SEARCH_SCAN,
771
+ }).documents.filter((entry) => entry.documentId !== document.documentId);
772
+ const nextSummaries = [
773
+ buildDocumentSummary(document),
774
+ ...summaries,
775
+ ].sort((left, right) => String(right.importedAt).localeCompare(String(left.importedAt)));
776
+ writeJsonl(paths.catalogPath, nextSummaries);
777
+ return document;
778
+ }
779
+
780
+ function scoreImportedDocument(document, tokens) {
781
+ const title = String(document.title || '');
782
+ const excerpt = String(document.excerpt || '');
783
+ const content = String(document.content || '');
784
+ const tags = safeArray(document.tags);
785
+ const proposalsText = safeArray(document.proposals)
786
+ .map((proposal) => [proposal.title, proposal.evidence, proposal.templateId].filter(Boolean).join(' '))
787
+ .join(' ');
788
+
789
+ let score = 0;
790
+ const matchedTokens = [];
791
+ for (const token of tokens) {
792
+ let tokenScore = 0;
793
+ tokenScore += Math.min(1, countMatches(title, token)) * 5;
794
+ tokenScore += Math.min(2, countMatches(excerpt, token)) * 3;
795
+ tokenScore += Math.min(3, countMatches(content, token)) * 1;
796
+ tokenScore += Math.min(2, countMatches(proposalsText, token)) * 2;
797
+ tokenScore += tags.some((tag) => String(tag).toLowerCase().includes(token)) ? 2 : 0;
798
+ if (tokenScore > 0) {
799
+ matchedTokens.push(token);
800
+ score += tokenScore;
801
+ }
802
+ }
803
+
804
+ const phrase = tokens.join(' ');
805
+ if (phrase && title.toLowerCase().includes(phrase)) {
806
+ score += 4;
807
+ }
808
+ if (phrase && excerpt.toLowerCase().includes(phrase)) {
809
+ score += 2;
810
+ }
811
+
812
+ return {
813
+ score,
814
+ matchedTokens,
815
+ };
816
+ }
817
+
818
+ function searchImportedDocuments(options = {}) {
819
+ const query = String(options.query || '').trim();
820
+ if (!query) {
821
+ throw new Error('query is required');
822
+ }
823
+
824
+ const tokens = tokenize(query);
825
+ const docs = listImportedDocuments({
826
+ ...options,
827
+ limit: MAX_SEARCH_SCAN,
828
+ query: '',
829
+ }).documents
830
+ .map((summary) => readImportedDocument(summary.documentId, options))
831
+ .filter(Boolean)
832
+ .map((document) => {
833
+ const scored = scoreImportedDocument(document, tokens);
834
+ return {
835
+ ...document,
836
+ _score: Number(scored.score.toFixed(4)),
837
+ _matchedTokens: scored.matchedTokens,
838
+ };
839
+ })
840
+ .filter((document) => document._score > 0)
841
+ .sort((left, right) => {
842
+ if (right._score !== left._score) return right._score - left._score;
843
+ return String(right.importedAt).localeCompare(String(left.importedAt));
844
+ });
845
+
846
+ const limit = Number.isFinite(Number(options.limit))
847
+ ? Math.max(1, Math.min(50, Number(options.limit)))
848
+ : 10;
849
+ return docs.slice(0, limit);
850
+ }
851
+
852
+ function importDocument(options = {}) {
853
+ const hasFilePath = Boolean(options.filePath);
854
+ const hasContent = typeof options.content === 'string' && options.content.trim().length > 0;
855
+ if (!hasFilePath && !hasContent) {
856
+ throw new Error('filePath or content is required');
857
+ }
858
+
859
+ const sourcePath = hasFilePath ? path.resolve(String(options.filePath)) : null;
860
+ if (sourcePath && !fs.existsSync(sourcePath)) {
861
+ throw new Error(`Path does not exist: ${sourcePath}`);
862
+ }
863
+
864
+ const rawContent = hasContent
865
+ ? String(options.content)
866
+ : fs.readFileSync(sourcePath, 'utf8');
867
+ const sourceFormat = inferSourceFormat(sourcePath, options.sourceFormat);
868
+ if (!sourceFormat) {
869
+ throw new Error('Unsupported document format. Supported formats: markdown, text, yaml, json, html.');
870
+ }
871
+
872
+ const normalizedContent = normalizeDocumentBody(rawContent, sourceFormat);
873
+ if (!normalizedContent) {
874
+ throw new Error('document content is empty after normalization');
875
+ }
876
+
877
+ const title = extractTitle({
878
+ explicitTitle: options.title,
879
+ filePath: sourcePath,
880
+ rawContent,
881
+ normalizedContent,
882
+ sourceFormat,
883
+ });
884
+ const fingerprint = sha256(`${title}\n${normalizedContent}`);
885
+ const importedAt = nowIso();
886
+ const sourceName = sourcePath ? path.basename(sourcePath) : null;
887
+ const documentId = `doc_${slugify(title || sourceName || 'document').slice(0, 24) || 'document'}_${fingerprint.slice(0, 12)}`;
888
+ const document = {
889
+ documentId,
890
+ title,
891
+ sourceType: sourcePath ? 'file' : 'inline',
892
+ sourcePath,
893
+ sourceName,
894
+ sourceFormat,
895
+ sourceUrl: options.sourceUrl ? String(options.sourceUrl).trim() : null,
896
+ importedAt,
897
+ tags: normalizeTags(options.tags),
898
+ fingerprint,
899
+ excerpt: buildExcerpt(normalizedContent),
900
+ content: normalizedContent,
901
+ contentBytes: Buffer.byteLength(normalizedContent, 'utf8'),
902
+ lineCount: normalizedContent.split('\n').filter(Boolean).length,
903
+ headings: extractHeadings(normalizedContent),
904
+ };
905
+ document.proposals = options.proposeGates === false
906
+ ? []
907
+ : proposeGatesFromDocument(document, options);
908
+ document.matchedTemplateIds = document.proposals
909
+ .map((proposal) => proposal.templateId)
910
+ .filter(Boolean);
911
+
912
+ persistDocument(document, options);
913
+ return document;
914
+ }
915
+
916
+ module.exports = {
917
+ DOCUMENTS_DIRNAME,
918
+ DOCUMENT_CATALOG_FILENAME,
919
+ getDocumentStorePaths,
920
+ getDocumentPath,
921
+ importDocument,
922
+ listImportedDocuments,
923
+ normalizeDocumentBody,
924
+ proposeGatesFromDocument,
925
+ readImportedDocument,
926
+ searchImportedDocuments,
927
+ };