clearai-dsh 0.1.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 (154) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +201 -0
  3. package/README.md +138 -0
  4. package/README.zh-CN.md +138 -0
  5. package/bin/clearai.mjs +224 -0
  6. package/brand/README.md +41 -0
  7. package/brand/logo-512-dark.png +0 -0
  8. package/brand/logo-512.png +0 -0
  9. package/brand/logo-lockup-dark.png +0 -0
  10. package/brand/logo-lockup.png +0 -0
  11. package/brand/logo-lockup.svg +12 -0
  12. package/brand/logo-wordmark.svg +6 -0
  13. package/brand/logo.svg +19 -0
  14. package/cordis.patch.yml +39 -0
  15. package/lib/client.js +3071 -0
  16. package/lib/fold.js +1576 -0
  17. package/lib/host.js +605 -0
  18. package/package.json +65 -0
  19. package/presets/clearai/agent.cordis.yml +226 -0
  20. package/presets/clearai/plugins/brain.js +547 -0
  21. package/presets/clearai/plugins/clearai-kernel.js +5485 -0
  22. package/presets/clearai/plugins/ontology.js +306 -0
  23. package/presets/clearai/plugins/prompts.js +312 -0
  24. package/presets/clearai/preset.yml +5 -0
  25. package/presets/clearai/skills/clearai-loop/SKILL.md +89 -0
  26. package/presets/clearai/template/knowledge/README.md +25 -0
  27. package/presets/clearai/template/memory/README.md +34 -0
  28. package/presets/clearai/template/project.md +49 -0
  29. package/presets/clearai/template/skills/README.md +37 -0
  30. package/presets/clearai/template/skills/chart-diagram-qa/SKILL.md +43 -0
  31. package/presets/clearai/template/skills/citation-management/SKILL.md +73 -0
  32. package/presets/clearai/template/skills/citation-management/references/bibtex_formatting.md +908 -0
  33. package/presets/clearai/template/skills/citation-management/references/citation_validation.md +794 -0
  34. package/presets/clearai/template/skills/citation-management/references/google_scholar_search.md +725 -0
  35. package/presets/clearai/template/skills/citation-management/references/metadata_extraction.md +870 -0
  36. package/presets/clearai/template/skills/citation-management/references/pubmed_search.md +839 -0
  37. package/presets/clearai/template/skills/citation-management/scripts/doi_to_bibtex.py +204 -0
  38. package/presets/clearai/template/skills/citation-management/scripts/extract_metadata.py +569 -0
  39. package/presets/clearai/template/skills/citation-management/scripts/format_bibtex.py +349 -0
  40. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic.py +139 -0
  41. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic_ai.py +817 -0
  42. package/presets/clearai/template/skills/citation-management/scripts/search_google_scholar.py +282 -0
  43. package/presets/clearai/template/skills/citation-management/scripts/search_pubmed.py +398 -0
  44. package/presets/clearai/template/skills/citation-management/scripts/validate_citations.py +497 -0
  45. package/presets/clearai/template/skills/data-analysis/SKILL.md +92 -0
  46. package/presets/clearai/template/skills/data-analysis/checklists/readiness_check.md +23 -0
  47. package/presets/clearai/template/skills/data-analysis/templates/analysis_report.md.tpl +63 -0
  48. package/presets/clearai/template/skills/data-analysis/templates/cleaning_rules_draft.yaml.tpl +32 -0
  49. package/presets/clearai/template/skills/data-analysis/templates/data_dictionary.md.tpl +12 -0
  50. package/presets/clearai/template/skills/data-analysis/templates/domain_knowledge_template.md.tpl +316 -0
  51. package/presets/clearai/template/skills/data-analysis/templates/feature_candidates.json.tpl +20 -0
  52. package/presets/clearai/template/skills/data-analysis/templates/quality_scorecard.md.tpl +30 -0
  53. package/presets/clearai/template/skills/data-analysis/workflows/01-data-profiling.md +42 -0
  54. package/presets/clearai/template/skills/data-analysis/workflows/02-quality-audit.md +36 -0
  55. package/presets/clearai/template/skills/data-analysis/workflows/03-physical-correlation.md +25 -0
  56. package/presets/clearai/template/skills/data-analysis/workflows/04-unstructured-mining.md +26 -0
  57. package/presets/clearai/template/skills/data-qa-analysis/SKILL.md +102 -0
  58. package/presets/clearai/template/skills/data-qa-analysis/checklists/readiness_check.md +62 -0
  59. package/presets/clearai/template/skills/data-qa-analysis/templates/best_in_class_report.md.tpl +56 -0
  60. package/presets/clearai/template/skills/data-qa-analysis/templates/cleaning_rules_draft.yaml.tpl +56 -0
  61. package/presets/clearai/template/skills/data-qa-analysis/templates/data_dictionary.md.tpl +13 -0
  62. package/presets/clearai/template/skills/data-qa-analysis/templates/data_source_inventory_and_lineage.md.tpl +146 -0
  63. package/presets/clearai/template/skills/data-qa-analysis/templates/data_status_report.md.tpl +60 -0
  64. package/presets/clearai/template/skills/data-qa-analysis/templates/steady_state_rules.yaml.tpl +41 -0
  65. package/presets/clearai/template/skills/data-qa-analysis/templates/subsystem_registry.md.tpl +101 -0
  66. package/presets/clearai/template/skills/data-qa-analysis/templates/unified_execution_plan.md.tpl +100 -0
  67. package/presets/clearai/template/skills/data-qa-analysis/workflows/01-data-source-inventory-and-lineage.md +194 -0
  68. package/presets/clearai/template/skills/data-qa-analysis/workflows/02-data-alignment-and-tag-semantics.md +122 -0
  69. package/presets/clearai/template/skills/data-qa-analysis/workflows/03-steady-state-identification.md +126 -0
  70. package/presets/clearai/template/skills/data-qa-analysis/workflows/04-consumption-analysis.md +152 -0
  71. package/presets/clearai/template/skills/data-qa-analysis/workflows/05-best-in-class-and-optimization-space.md +78 -0
  72. package/presets/clearai/template/skills/domain-presearch/SKILL.md +131 -0
  73. package/presets/clearai/template/skills/domain-presearch/checklists/domain_checklist.md +24 -0
  74. package/presets/clearai/template/skills/domain-presearch/references/figure_code.md +78 -0
  75. package/presets/clearai/template/skills/domain-presearch/references/strategic_frameworks.md +38 -0
  76. package/presets/clearai/template/skills/exploration-loop/SKILL.md +81 -0
  77. package/presets/clearai/template/skills/exploratory-data-analysis/SKILL.md +77 -0
  78. package/presets/clearai/template/skills/exploratory-data-analysis/references/bioinformatics_genomics_formats.md +664 -0
  79. package/presets/clearai/template/skills/exploratory-data-analysis/references/chemistry_molecular_formats.md +664 -0
  80. package/presets/clearai/template/skills/exploratory-data-analysis/references/general_scientific_formats.md +518 -0
  81. package/presets/clearai/template/skills/exploratory-data-analysis/references/microscopy_imaging_formats.md +620 -0
  82. package/presets/clearai/template/skills/exploratory-data-analysis/references/proteomics_metabolomics_formats.md +517 -0
  83. package/presets/clearai/template/skills/exploratory-data-analysis/references/spectroscopy_analytical_formats.md +633 -0
  84. package/presets/clearai/template/skills/exploratory-data-analysis/scripts/eda_analyzer.py +547 -0
  85. package/presets/clearai/template/skills/hypothesis-generation/SKILL.md +73 -0
  86. package/presets/clearai/template/skills/hypothesis-generation/references/experimental_design_patterns.md +329 -0
  87. package/presets/clearai/template/skills/hypothesis-generation/references/hypothesis_quality_criteria.md +198 -0
  88. package/presets/clearai/template/skills/hypothesis-generation/references/literature_search_strategies.md +622 -0
  89. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic.py +139 -0
  90. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic_ai.py +817 -0
  91. package/presets/clearai/template/skills/literature-review/SKILL.md +72 -0
  92. package/presets/clearai/template/skills/literature-review/references/citation_styles.md +166 -0
  93. package/presets/clearai/template/skills/literature-review/references/database_strategies.md +455 -0
  94. package/presets/clearai/template/skills/literature-review/scripts/generate_pdf.py +176 -0
  95. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic.py +139 -0
  96. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic_ai.py +817 -0
  97. package/presets/clearai/template/skills/literature-review/scripts/search_databases.py +303 -0
  98. package/presets/clearai/template/skills/literature-review/scripts/verify_citations.py +221 -0
  99. package/presets/clearai/template/skills/paper-lookup/SKILL.md +59 -0
  100. package/presets/clearai/template/skills/paper-lookup/references/arxiv.md +161 -0
  101. package/presets/clearai/template/skills/paper-lookup/references/biorxiv.md +118 -0
  102. package/presets/clearai/template/skills/paper-lookup/references/core.md +150 -0
  103. package/presets/clearai/template/skills/paper-lookup/references/crossref.md +181 -0
  104. package/presets/clearai/template/skills/paper-lookup/references/medrxiv.md +104 -0
  105. package/presets/clearai/template/skills/paper-lookup/references/openalex.md +174 -0
  106. package/presets/clearai/template/skills/paper-lookup/references/pmc.md +152 -0
  107. package/presets/clearai/template/skills/paper-lookup/references/pubmed.md +124 -0
  108. package/presets/clearai/template/skills/paper-lookup/references/semantic-scholar.md +203 -0
  109. package/presets/clearai/template/skills/paper-lookup/references/unpaywall.md +127 -0
  110. package/presets/clearai/template/skills/process-presearch/SKILL.md +196 -0
  111. package/presets/clearai/template/skills/process-presearch/checklists/process_checklist.md +18 -0
  112. package/presets/clearai/template/skills/process-presearch/references/figure_code.md +107 -0
  113. package/presets/clearai/template/skills/process-presearch/references/source_attribution_example.md +22 -0
  114. package/presets/clearai/template/skills/process-understanding-extraction/SKILL.md +69 -0
  115. package/presets/clearai/template/skills/process-understanding-extraction/checklists/readiness_check.md +34 -0
  116. package/presets/clearai/template/skills/process-understanding-extraction/templates/docx_raw_dump_extractor.py.tpl +132 -0
  117. package/presets/clearai/template/skills/process-understanding-extraction/templates/entity_map_unit_topology.json.tpl +86 -0
  118. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief.md.tpl +89 -0
  119. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief_builder_from_raw_dump.py.tpl +203 -0
  120. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_flow_mermaid.md.tpl +41 -0
  121. package/presets/clearai/template/skills/process-understanding-extraction/templates/unified_execution_plan.md.tpl +53 -0
  122. package/presets/clearai/template/skills/process-understanding-extraction/workflows/01-process-doc-discovery.md +173 -0
  123. package/presets/clearai/template/skills/process-understanding-extraction/workflows/02-process-understanding-and-diagramming.md +106 -0
  124. package/presets/clearai/template/skills/scientific-brainstorming/SKILL.md +64 -0
  125. package/presets/clearai/template/skills/scientific-brainstorming/references/brainstorming_methods.md +326 -0
  126. package/presets/clearai/template/skills/scientific-critical-thinking/SKILL.md +72 -0
  127. package/presets/clearai/template/skills/scientific-critical-thinking/references/common_biases.md +364 -0
  128. package/presets/clearai/template/skills/scientific-critical-thinking/references/evidence_hierarchy.md +485 -0
  129. package/presets/clearai/template/skills/scientific-critical-thinking/references/experimental_design.md +496 -0
  130. package/presets/clearai/template/skills/scientific-critical-thinking/references/logical_fallacies.md +478 -0
  131. package/presets/clearai/template/skills/scientific-critical-thinking/references/scientific_method.md +169 -0
  132. package/presets/clearai/template/skills/scientific-critical-thinking/references/statistical_pitfalls.md +506 -0
  133. package/presets/clearai/template/skills/skill-creator/SKILL.md +109 -0
  134. package/presets/clearai/template/skills/skill-creator/references/authoring-guide.md +89 -0
  135. package/presets/clearai/template/skills/statistical-analysis/SKILL.md +79 -0
  136. package/presets/clearai/template/skills/statistical-analysis/references/assumptions_and_diagnostics.md +369 -0
  137. package/presets/clearai/template/skills/statistical-analysis/references/bayesian_statistics.md +653 -0
  138. package/presets/clearai/template/skills/statistical-analysis/references/effect_sizes_and_power.md +578 -0
  139. package/presets/clearai/template/skills/statistical-analysis/references/reporting_standards.md +469 -0
  140. package/presets/clearai/template/skills/statistical-analysis/references/test_selection_guide.md +129 -0
  141. package/presets/clearai/template/skills/statistical-analysis/scripts/assumption_checks.py +538 -0
  142. package/presets/clearai/template/skills/web-artifact/SKILL.md +165 -0
  143. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.css +229 -0
  144. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.js +373 -0
  145. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/LICENSE +263 -0
  146. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/UPSTREAM.md +26 -0
  147. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/elk.bundled.js +6605 -0
  148. package/presets/clearai/template/skills/web-artifact/references/when-drawing-a-topology.md +150 -0
  149. package/presets/clearai/template/skills/web-artifact/references/when-the-page-must-work-offline.md +62 -0
  150. package/presets/clearai/template/skills/web-artifact/scripts/check_artifact.py +167 -0
  151. package/presets/clearai/template/skills/web-artifact/scripts/render_topology.js +272 -0
  152. package/presets/clearai/template/skills/what-if-oracle/LICENSE.txt +5 -0
  153. package/presets/clearai/template/skills/what-if-oracle/SKILL.md +72 -0
  154. package/presets/clearai/template/skills/what-if-oracle/references/scenario-templates.md +154 -0
@@ -0,0 +1,547 @@
1
+ /**
2
+ * brain —— 外脑的**投影层**(预设平面)。
3
+ *
4
+ * 第一性原理:外脑只需要四件事——落盘、有界索引、
5
+ * 按需读全文、字节稳定。这四件在 DSH 里都有原生承载:
6
+ *
7
+ * L1 索引 宿主技能目录(`skills` 服务 + `tool-skill` 的 **digest 变了才注入**)
8
+ * L2 全文 原生 `skill` 工具(按名字加载)
9
+ * L3 单条 `resourceBase` 指向目录,模型用原生 `read`(带 limit/offset)按需读
10
+ *
11
+ * 所以这个模块**不建索引、不做缓存、不注入任何东西**:它只是把工作区里的两类文件
12
+ * **投影成目录条目**——
13
+ * · `clear/skills/<name>/SKILL.md` → 一个技能条目(ClearAI 的 19 个技能就是这个形状)
14
+ * · `clear/memory/**` → **一个虚拟条目** `project-memory`:它的 L2 正文是现算的
15
+ * (所以不存在「INDEX.md 过期」这种状态),`resourceBase` 指向记忆目录,单条按需读。
16
+ *
17
+ * 自建的东西只有两件工具(`SaveSkill` / `WriteMemory`),因为「结构」只有机制保证得了:
18
+ * 技能必须带 name/description 且**默认是候选态**(模型写的 SOP 该由人过一道);
19
+ * 教训必须带 Context/Trigger/Action/Validation/Reuse Hint,事实必须带 Statement/Evidence/Scope/Last Verified,
20
+ * 而且按标题跨文件去重。
21
+ *
22
+ * 字段契约由本模块定义并强制:lesson 与 fact 各有必填字段,按标题跨文件去重,
23
+ * 虚拟条目的正文有界截断。
24
+ */
25
+
26
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
27
+ import { join, relative } from 'node:path'
28
+
29
+ /** lesson / fact 的字段契约。 */
30
+ export const LESSON_REQUIRED = ['Context', 'Trigger', 'Action', 'Validation', 'Reuse Hint']
31
+ export const FACT_REQUIRED = ['Statement', 'Evidence', 'Scope', 'Last Verified']
32
+ export const LESSON_FIELD_ORDER = ['Context', 'Trigger', 'Symptom', 'Root Cause', 'Action', 'Fix', 'Validation', 'Reuse Hint']
33
+ export const FACT_FIELD_ORDER = ['Statement', 'Evidence', 'Scope', 'Last Verified']
34
+ /** 虚拟条目的正文预算(只用来**截断**,不用于自建注入)。 */
35
+ export const MEMORY_INDEX_CHAR_BUDGET = 1500
36
+ export const MAX_TITLES_PER_FILE = 5
37
+ const ENTRY_HEADING = /^##\s+(Lesson|Fact):\s+(.+?)\s*$/gm
38
+ /** 技能名:kebab-case(目录名即名字)。 */
39
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
40
+
41
+ export function brainPaths(cwd) {
42
+ return {
43
+ skills: join(cwd, 'clear', 'skills'),
44
+ memory: join(cwd, 'clear', 'memory'),
45
+ }
46
+ }
47
+
48
+ function listDir(dir) {
49
+ try {
50
+ return readdirSync(dir)
51
+ } catch {
52
+ return []
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 极简 frontmatter 解析:只认 `key: value` 与块标量(`|` / `>`),以及一层 `metadata:`。
58
+ * 不为 YAML 写一个通用解析器——ClearAI 的技能 frontmatter 就是这么几件事
59
+ * (`name` / `description`(块标量,带【触发词】与适用范围)/ `version` / `metadata.{tier,origin,created_at}`)。
60
+ */
61
+ export function parseFrontmatter(text) {
62
+ if (typeof text !== 'string' || !text.startsWith('---')) return { data: {}, body: text ?? '' }
63
+ const end = text.indexOf('\n---', 3)
64
+ if (end < 0) return { data: {}, body: text }
65
+ const head = text.slice(3, end).replace(/^\n/, '')
66
+ const body = text.slice(end + 4).replace(/^\n+/, '')
67
+ const data = {}
68
+ let nested = null
69
+ let blockKey = null
70
+ let blockIndent = null
71
+ let blockLines = []
72
+ const flushBlock = () => {
73
+ if (blockKey === null) return
74
+ const value = blockLines.join('\n').trim()
75
+ if (nested === null) data[blockKey] = value
76
+ else data[nested] = { ...(data[nested] ?? {}), [blockKey]: value }
77
+ blockKey = null
78
+ blockIndent = null
79
+ blockLines = []
80
+ }
81
+ for (const raw of head.split('\n')) {
82
+ const indent = raw.length - raw.trimStart().length
83
+ if (blockKey !== null) {
84
+ // 块标量的缩进以**第一行内容**为准(不是固定 +1):YAML 就是这么定的。
85
+ if (blockIndent === null && raw.trim() !== '') blockIndent = indent
86
+ if (raw.trim() === '' || (blockIndent !== null && indent >= blockIndent)) {
87
+ blockLines.push(raw.trim() === '' ? '' : raw.slice(blockIndent ?? indent))
88
+ continue
89
+ }
90
+ flushBlock()
91
+ }
92
+ const line = raw.trim()
93
+ if (line === '' || line.startsWith('#')) continue
94
+ const match = /^([A-Za-z_][\w.-]*):\s*(.*)$/.exec(line)
95
+ if (match === null) continue
96
+ const [, key, rest] = match
97
+ if (rest === '' && indent === 0 && key === 'metadata') {
98
+ nested = 'metadata'
99
+ data.metadata = data.metadata ?? {}
100
+ continue
101
+ }
102
+ if (rest === '|' || rest === '>') {
103
+ blockKey = key
104
+ blockIndent = null
105
+ blockLines = []
106
+ continue
107
+ }
108
+ const value = rest.trim().replace(/^["']|["']$/g, '')
109
+ if (nested !== null && indent > 0) data[nested] = { ...(data[nested] ?? {}), [key]: value }
110
+ else {
111
+ nested = null
112
+ data[key] = value
113
+ }
114
+ }
115
+ flushBlock()
116
+ return { data, body }
117
+ }
118
+
119
+ /** 技能条目:只报**带 SKILL.md 的目录**(ClearAI 的 19 个技能都是这个形状)。 */
120
+ export function skillEntries(cwd) {
121
+ const root = brainPaths(cwd).skills
122
+ const entries = []
123
+ for (const name of listDir(root)) {
124
+ const dir = join(root, name)
125
+ let info = null
126
+ try {
127
+ info = statSync(dir)
128
+ } catch {
129
+ continue
130
+ }
131
+ if (!info.isDirectory()) continue
132
+ const file = join(dir, 'SKILL.md')
133
+ if (!existsSync(file)) continue
134
+ let text = ''
135
+ try {
136
+ text = readFileSync(file, 'utf8')
137
+ } catch {
138
+ continue
139
+ }
140
+ const { data, body } = parseFrontmatter(text)
141
+ const declared = typeof data.name === 'string' && data.name !== '' ? data.name : name
142
+ entries.push({
143
+ name: declared,
144
+ dirName: name,
145
+ description: typeof data.description === 'string' ? data.description : '',
146
+ status: typeof data.status === 'string' ? data.status : 'active',
147
+ tier: data.metadata?.tier ?? null,
148
+ version: data.version ?? null,
149
+ dir,
150
+ file,
151
+ body,
152
+ bytes: Buffer.byteLength(text, 'utf8'),
153
+ })
154
+ }
155
+ return entries.sort((left, right) => left.name.localeCompare(right.name))
156
+ }
157
+
158
+ /** 记忆条目:扁平 `.md`,`## Lesson: <title>` / `## Fact: <title>` 两种块(ClearAI 的格式)。 */
159
+ export function memoryEntries(cwd) {
160
+ const root = brainPaths(cwd).memory
161
+ const entries = []
162
+ for (const file of listDir(root)) {
163
+ if (!file.endsWith('.md')) continue
164
+ const path = join(root, file)
165
+ let text = ''
166
+ try {
167
+ text = readFileSync(path, 'utf8')
168
+ } catch {
169
+ continue
170
+ }
171
+ for (const match of text.matchAll(ENTRY_HEADING)) {
172
+ const kind = match[1].toLowerCase()
173
+ const title = match[2].trim()
174
+ const after = text.slice(match.index + match[0].length)
175
+ const until = after.search(/\n##\s+(?:Lesson|Fact):/)
176
+ const block = until < 0 ? after : after.slice(0, until)
177
+ const summary = (block.split('\n').find((line) => line.trim().startsWith('- ')) ?? '').replace(/^-\s*/, '').trim()
178
+ entries.push({ file, path, kind, title, summary: summary.slice(0, 160) })
179
+ }
180
+ }
181
+ return entries.sort((left, right) => left.title.localeCompare(right.title))
182
+ }
183
+
184
+ /** 技能条目的目录摘要:name + description(ClearAI 的索引也只给这两样)。 */
185
+ export function skillSummaryLine(entry) {
186
+ const tag = entry.status === 'candidate' ? '[候选·待采纳]' : ''
187
+ return `${tag}${entry.description || '(未写 description)'}`
188
+ }
189
+
190
+ /** 记忆虚拟条目的 L1 摘要:有界(ClearAI `MEMORY_INDEX_CHAR_BUDGET` 的用意一样)。 */
191
+ export function memoryIndexDigest(entries) {
192
+ if (entries.length === 0) return ''
193
+ const titles = entries.map((entry) => `${entry.kind === 'lesson' ? '经验' : '事实'}:${entry.title}`)
194
+ const head = `本项目已沉淀 ${entries.length} 条:`
195
+ let line = head
196
+ for (const title of titles) {
197
+ if (line.length + title.length + 1 > 480) {
198
+ line += '…'
199
+ break
200
+ }
201
+ line += `${line === head ? '' : '、'}${title}`
202
+ }
203
+ return line
204
+ }
205
+
206
+ /** 记忆虚拟条目的 L2 正文(现算,所以不存在「索引过期」)。 */
207
+ export function memoryIndexBody(entries, { budget = MEMORY_INDEX_CHAR_BUDGET } = {}) {
208
+ if (entries.length === 0) return ''
209
+ const byFile = new Map()
210
+ for (const entry of entries) {
211
+ if (!byFile.has(entry.file)) byFile.set(entry.file, [])
212
+ byFile.get(entry.file).push(entry)
213
+ }
214
+ const lines = [
215
+ '# 本项目记忆(索引)',
216
+ '',
217
+ `共 ${entries.length} 条,分布在 ${byFile.size} 个文件里。**这一页只是索引**:读某一条用 \`read\` 打开下面标的文件(可用 offset 续读);要写新的一条用 \`WriteMemory\`(它做字段校验与标题去重)。`,
218
+ '',
219
+ ]
220
+ let used = lines.join('\n').length
221
+ for (const [file, items] of byFile) {
222
+ const shown = items.slice(0, MAX_TITLES_PER_FILE)
223
+ const block = [`## ${file}`, ...shown.map((item) => `- [${item.kind}] ${item.title}${item.summary === '' ? '' : ` — ${item.summary}`}`)]
224
+ if (items.length > shown.length) block.push(`- …另有 ${items.length - shown.length} 条(读该文件看全文)`)
225
+ const text = `${block.join('\n')}\n`
226
+ if (used + text.length > budget * 4) {
227
+ lines.push(`## ${file}`, `- …本文件另有 ${items.length} 条(超出索引预算,读文件看全文)`, '')
228
+ used += 40
229
+ continue
230
+ }
231
+ lines.push(...block, '')
232
+ used += text.length
233
+ }
234
+ lines.push('> 领域知识在 `clear/knowledge/`(按需 read);项目宪法在 `PROJECT.md`(宿主每回合注入)。')
235
+ return lines.join('\n')
236
+ }
237
+
238
+ // ── 目录条目:技能与记忆都投影成条目,交给宿主原生的 skills 服务 ────────────────
239
+
240
+ export const BRAIN_PROVIDER = 'clearai-brain'
241
+ export const MEMORY_ENTRY = 'project-memory'
242
+
243
+ /** 指纹:目录里所有相关文件的 (路径, mtime, 大小)。变了才重算(与宿主 digest 注入同一套纪律)。 */
244
+ export function brainFingerprint(cwd) {
245
+ const parts = []
246
+ for (const entry of skillEntries(cwd)) parts.push(`${entry.file}:${statSync(entry.file).mtimeMs}:${entry.bytes}`)
247
+ for (const entry of memoryEntries(cwd)) parts.push(`${entry.path}:${statSync(entry.path).mtimeMs}`)
248
+ return parts.join('|')
249
+ }
250
+
251
+ /**
252
+ * 造一个技能提供者。它**不缓存目录**(缓存交给宿主的目录 digest 与我们的指纹短路),
253
+ * 只做两件事:把工作区投影成条目;把候选技能标成「模型不可调用」。
254
+ *
255
+ * `invalidate`(`registerProvider` 给的 `control.invalidate`)在**指纹真的变了**时被调用:
256
+ * 宿主那边另有一层按「cwd + 作用域 + revision」做键的目录缓存,不 bump revision 的话,
257
+ * 我们这边明明多了一条技能、它那边还会把旧表端出来(实测:第一回合的目录被缓存成空表,
258
+ * 之后连模型都一直看不到 `clear/skills`——2026-09-11 的 E2E 抓到的就是这个)。
259
+ * 原生 filesystem provider 靠 watcher 做这件事,我们没有 watcher,就靠指纹。
260
+ */
261
+ export function createBrainProvider(options = {}) {
262
+ const log = typeof options.log === 'function' ? options.log : () => {}
263
+ const invalidate = typeof options.invalidate === 'function' ? options.invalidate : null
264
+ let fingerprint = null
265
+ let snapshot = { skills: [], memory: [] }
266
+ const refresh = (cwd) => {
267
+ const next = brainFingerprint(cwd)
268
+ if (next === fingerprint) return snapshot
269
+ // 第一次观测不算「变了」;之后的每一次变化都要让宿主的目录缓存失效。
270
+ const changed = fingerprint !== null
271
+ fingerprint = next
272
+ snapshot = { skills: skillEntries(cwd), memory: memoryEntries(cwd) }
273
+ if (changed && invalidate !== null) {
274
+ try {
275
+ invalidate()
276
+ } catch (error) {
277
+ log(`目录失效失败:${String(error?.message ?? error)}`)
278
+ }
279
+ }
280
+ return snapshot
281
+ }
282
+ const candidatesFor = (cwd) => {
283
+ if (typeof cwd !== 'string' || cwd === '') return { candidates: [], complete: true }
284
+ let current
285
+ try {
286
+ current = refresh(cwd)
287
+ } catch (error) {
288
+ log(`外脑扫描失败:${String(error?.message ?? error)}`)
289
+ return { candidates: [], complete: false }
290
+ }
291
+ const candidates = []
292
+ for (const entry of current.skills) {
293
+ // 候选态 = 模型看不见、人也加载不了它当 SOP;人采纳后改 frontmatter 才生效。
294
+ // 这是 ClearAI「候选 → 收件箱采纳 → 生效」那条治理的**原生表达**(invocation 策略)。
295
+ const invocable = entry.status !== 'candidate'
296
+ candidates.push({
297
+ name: entry.name,
298
+ description: skillSummaryLine(entry),
299
+ whenToUse: entry.description.slice(0, 500),
300
+ invocation: { modelInvocable: invocable, userInvocable: true },
301
+ source: entry.tier === 'system' ? 'clearai-template' : 'clearai-workspace',
302
+ provider: BRAIN_PROVIDER,
303
+ resourceBase: { kind: 'directory', path: entry.dir },
304
+ rank: invocable ? 100 : 60,
305
+ path: entry.file,
306
+ metadata: { status: entry.status, version: entry.version, tier: entry.tier, bytes: entry.bytes },
307
+ locator: { kind: 'skill', name: entry.name, dir: entry.dir, file: entry.file },
308
+ })
309
+ }
310
+ if (current.memory.length > 0) {
311
+ candidates.push({
312
+ name: MEMORY_ENTRY,
313
+ description: memoryIndexDigest(current.memory),
314
+ invocation: { modelInvocable: true, userInvocable: true },
315
+ source: 'clearai-memory',
316
+ provider: BRAIN_PROVIDER,
317
+ resourceBase: { kind: 'directory', path: brainPaths(cwd).memory },
318
+ rank: 80,
319
+ locator: { kind: 'memory', cwd },
320
+ })
321
+ }
322
+ return { candidates, complete: true }
323
+ }
324
+ return {
325
+ name: BRAIN_PROVIDER,
326
+ async list(lookup = {}) {
327
+ return candidatesFor(lookup?.cwd)
328
+ },
329
+ async get(candidate, lookup = {}) {
330
+ const locator = candidate?.locator
331
+ if (locator === null || typeof locator !== 'object') return undefined
332
+ if (locator.kind === 'skill') {
333
+ let text = ''
334
+ try {
335
+ text = readFileSync(locator.file, 'utf8')
336
+ } catch {
337
+ return undefined
338
+ }
339
+ const { body } = parseFrontmatter(text)
340
+ return {
341
+ name: candidate.name,
342
+ description: candidate.description,
343
+ invocation: candidate.invocation,
344
+ source: candidate.source,
345
+ provider: BRAIN_PROVIDER,
346
+ resourceBase: { kind: 'directory', path: locator.dir },
347
+ path: locator.file,
348
+ // 正文就是 SKILL.md 的 body(宿主原生也是这么给的:`parsed.body.trim()`)。
349
+ content: body.trim(),
350
+ }
351
+ }
352
+ if (locator.kind === 'memory') {
353
+ const cwd = typeof lookup.cwd === 'string' && lookup.cwd !== '' ? lookup.cwd : locator.cwd
354
+ const entries = memoryEntries(cwd)
355
+ const body = memoryIndexBody(entries)
356
+ if (body === '') return undefined
357
+ return {
358
+ name: MEMORY_ENTRY,
359
+ description: memoryIndexDigest(entries),
360
+ invocation: { modelInvocable: true, userInvocable: true },
361
+ source: 'clearai-memory',
362
+ provider: BRAIN_PROVIDER,
363
+ resourceBase: { kind: 'directory', path: brainPaths(cwd).memory },
364
+ content: body,
365
+ }
366
+ }
367
+ return undefined
368
+ },
369
+ }
370
+ }
371
+
372
+ /**
373
+ * 外脑全景:面板要画的那份清单。
374
+ *
375
+ * 为什么由内核算而不由面板算:面板在浏览器里,**读不了盘**;而内核每次 pre-step 都在扫工作区。
376
+ * 所以清单随投影走(变了才发),内容才走宿主路由(点开某一条时才读)。
377
+ */
378
+ export function brainOverview(cwd) {
379
+ const skills = skillEntries(cwd).map((entry) => ({
380
+ name: entry.name,
381
+ description: entry.description.slice(0, 400),
382
+ status: entry.status,
383
+ tier: entry.tier,
384
+ version: entry.version,
385
+ bytes: entry.bytes,
386
+ // 资源文件数:技能是「目录式」的,正文之外的 workflows/templates/checklists 才是细节所在。
387
+ resources: (() => {
388
+ let count = 0
389
+ const walk = (dir, depth) => {
390
+ if (depth > 3) return
391
+ for (const name of listDir(dir)) {
392
+ const info = (() => {
393
+ try {
394
+ return statSync(join(dir, name))
395
+ } catch {
396
+ return null
397
+ }
398
+ })()
399
+ if (info === null) continue
400
+ if (info.isDirectory()) walk(join(dir, name), depth + 1)
401
+ else if (name !== 'SKILL.md') count += 1
402
+ }
403
+ }
404
+ walk(entry.dir, 0)
405
+ return count
406
+ })(),
407
+ path: `clear/skills/${entry.dirName}/SKILL.md`,
408
+ }))
409
+ const memory = memoryEntries(cwd)
410
+ return {
411
+ skills,
412
+ memory: {
413
+ count: memory.length,
414
+ files: [...new Set(memory.map((entry) => entry.file))].map((file) => ({
415
+ file,
416
+ path: `clear/memory/${file}`,
417
+ entries: memory.filter((entry) => entry.file === file).map((entry) => ({ kind: entry.kind, title: entry.title })),
418
+ })),
419
+ },
420
+ }
421
+ }
422
+
423
+ /**
424
+ * 等人采纳的候选技能(面板的收件箱据此出条目)。
425
+ * 状态锚:文件里的 `status: candidate` 在,条目就在;被采纳或被删,条目自然消失。
426
+ */
427
+ export function candidateSkills(cwd) {
428
+ return skillEntries(cwd)
429
+ .filter((entry) => entry.status === 'candidate')
430
+ .map((entry) => ({ name: entry.name, description: entry.description.slice(0, 300), dir: entry.dir }))
431
+ }
432
+
433
+ /**
434
+ * 采纳一个候选技能:把 frontmatter 里的 `status` 改成 `active`,并记下是谁、什么时候。
435
+ * 只改这一行——正文一个字都不动(技能是行为知识,采纳不该顺手改内容)。
436
+ * 返回 {ok, reason?}:文件不在、本来就不是候选,都如实说,不假装采纳成功。
437
+ */
438
+ export function promoteSkill(cwd, name, { at = null, by = 'user' } = {}) {
439
+ const problem = validateSkillName(name)
440
+ if (problem !== null) return { ok: false, reason: problem }
441
+ const entry = skillEntries(cwd).find((item) => item.name === name || item.dirName === name)
442
+ if (entry === undefined) return { ok: false, reason: 'skill_not_found' }
443
+ if (entry.status !== 'candidate') return { ok: false, reason: 'not_a_candidate', status: entry.status }
444
+ let text = ''
445
+ try {
446
+ text = readFileSync(entry.file, 'utf8')
447
+ } catch (error) {
448
+ return { ok: false, reason: 'read_failed' }
449
+ }
450
+ const end = text.indexOf('\n---', 3)
451
+ if (end < 0) return { ok: false, reason: 'frontmatter_missing' }
452
+ const head = text.slice(0, end)
453
+ const rest = text.slice(end)
454
+ const stamped = `${head.replace(/^status:\s*.*$/m, 'status: active')}\npromoted_by: ${by}\npromoted_at: ${at ?? new Date().toISOString()}`
455
+ try {
456
+ writeFileSync(entry.file, `${stamped}${rest}`, 'utf8')
457
+ } catch (error) {
458
+ return { ok: false, reason: 'write_failed' }
459
+ }
460
+ return { ok: true, path: entry.file, name: entry.name }
461
+ }
462
+
463
+ // ── 写入侧的两条规则(自建的两件工具就靠它们) ────────────────────────────────
464
+
465
+ /** 记忆文件名:扁平 `.md`,不许有路径分隔符或 `..`(ClearAI `validate_memory_filename`)。 */
466
+ export function validateMemoryFile(target) {
467
+ const name = String(target ?? '').trim()
468
+ if (name === '') return 'empty_target'
469
+ if (name.includes('/') || name.includes('\\') || name.includes('..')) return 'path_traversal'
470
+ if (!name.endsWith('.md')) return 'must_be_markdown'
471
+ return null
472
+ }
473
+
474
+ /** 技能名:kebab-case(ClearAI 的目录名即名字)。 */
475
+ export function validateSkillName(name) {
476
+ const value = String(name ?? '').trim()
477
+ if (value === '') return 'empty_name'
478
+ if (!SKILL_NAME.test(value)) return 'must_be_kebab_case'
479
+ return null
480
+ }
481
+
482
+ /** 技能目录内的相对路径:不许 `..`、不许绝对路径、不许反斜杠。 */
483
+ export function validateSkillPath(target) {
484
+ const value = String(target ?? 'SKILL.md').trim()
485
+ if (value === '') return 'empty_path'
486
+ if (value.startsWith('/') || value.includes('\\') || value.includes('..')) return 'path_traversal'
487
+ return null
488
+ }
489
+
490
+ export function lessonFieldsProblem(fields) {
491
+ const missing = LESSON_REQUIRED.filter((key) => String(fields?.[key] ?? '').trim() === '')
492
+ return missing.length === 0 ? null : missing
493
+ }
494
+
495
+ export function factFieldsProblem(fields) {
496
+ const missing = FACT_REQUIRED.filter((key) => String(fields?.[key] ?? '').trim() === '')
497
+ return missing.length === 0 ? null : missing
498
+ }
499
+
500
+ /** 一条记忆块的正文格式(ClearAI `format_lesson_block` / `format_fact_block` 的逐条对齐)。 */
501
+ export function formatMemoryBlock({ kind, title, fields, source = null, order }) {
502
+ const lines = [`## ${kind === 'lesson' ? 'Lesson' : 'Fact'}: ${String(title).trim()}`]
503
+ const seen = new Set()
504
+ for (const key of order) {
505
+ const value = String(fields?.[key] ?? '').trim()
506
+ if (value === '') continue
507
+ lines.push(`- ${key}: ${value}`)
508
+ seen.add(key)
509
+ }
510
+ for (const [key, raw] of Object.entries(fields ?? {})) {
511
+ if (seen.has(key)) continue
512
+ const value = String(raw ?? '').trim()
513
+ if (value === '') continue
514
+ lines.push(`- ${key}: ${value}`)
515
+ }
516
+ if (source !== null && String(source).trim() !== '') lines.push(`- Source: ${String(source).trim()}`)
517
+ return `${lines.join('\n')}\n`
518
+ }
519
+
520
+ /** 跨文件去重:任一记忆文件里已有同标题的同类条目就跳过(ClearAI 的 `entry_heading_exists`)。 */
521
+ export function memoryTitleExists(cwd, kind, title) {
522
+ const needle = `## ${kind === 'lesson' ? 'Lesson' : 'Fact'}: ${String(title).trim()}`
523
+ return memoryEntries(cwd).some((entry) => `${entry.kind === 'lesson' ? 'Lesson' : 'Fact'}: ${entry.title}` === needle.replace(/^##\s*/, ''))
524
+ }
525
+
526
+ /** 追加一条记忆;返回写到的文件与是否因为重复而跳过。 */
527
+ export function appendMemory(cwd, { kind, title, fields, source = null, target = null }) {
528
+ const file = target === null ? (kind === 'lesson' ? 'lessons.md' : 'facts.md') : validateMemoryFile(target) === null ? target : null
529
+ if (file === null) return { ok: false, reason: 'invalid_target' }
530
+ // 结构校验在**写入器**里,不在调用方:劝告保证不了「教训必须含那五项」,只有机制保证得了。
531
+ const missing = kind === 'lesson' ? lessonFieldsProblem(fields) : factFieldsProblem(fields)
532
+ if (missing !== null) return { ok: false, reason: 'missing_fields', missing }
533
+ if (memoryTitleExists(cwd, kind, title)) return { ok: true, skipped: true, path: join(brainPaths(cwd).memory, file) }
534
+ const dir = brainPaths(cwd).memory
535
+ mkdirSync(dir, { recursive: true })
536
+ const path = join(dir, file)
537
+ let existing = ''
538
+ try {
539
+ existing = readFileSync(path, 'utf8')
540
+ } catch {
541
+ existing = ''
542
+ }
543
+ const block = formatMemoryBlock({ kind, title, fields, source, order: kind === 'lesson' ? LESSON_FIELD_ORDER : FACT_FIELD_ORDER })
544
+ const separator = existing === '' ? '' : existing.endsWith('\n\n') ? '' : existing.endsWith('\n') ? '\n' : '\n\n'
545
+ writeFileSync(path, `${existing}${separator}${block}`, 'utf8')
546
+ return { ok: true, skipped: false, path, relative: relative(cwd, path) }
547
+ }