@microi.net/cli 5.9.5 → 5.9.6
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.
- package/.codebuddy-plugin/marketplace.json +2 -2
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.workbuddy-plugin/marketplace.json +2 -2
- package/.workbuddy-plugin/plugin.json +1 -1
- package/assets/build-meta.json +6 -6
- package/cordis.patch.yml +1 -1
- package/package.json +1 -1
- package/scripts/mcp-server.js +2 -1
- package/scripts/microi-cli.js +44 -49
- package/scripts/microi-codex-router.js +2 -1
- package/scripts/microi-skills.meta.json +238 -238
- package/skills/.microi-skills-version.json +2 -2
- package/skills/.progressive-disclosure-manifest.json +20 -20
- package/skills/ai-engine/agents/openai.yaml +4 -4
- package/skills/app-store/agents/openai.yaml +4 -4
- package/skills/datasource-engine/agents/openai.yaml +4 -4
- package/skills/job-engine/agents/openai.yaml +4 -4
- package/skills/message-notification/agents/openai.yaml +5 -5
- package/skills/microi-ai-app-auth.js +652 -652
- package/skills/microi-ai-application/agents/openai.yaml +4 -4
- package/skills/microi-codex-installer/SKILL.md +1 -1
- package/skills/microi-codex-installer/agents/openai.yaml +7 -7
- package/skills/microi-db-schema/agents/openai.yaml +4 -4
- package/skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +894 -894
- package/skills/microi-solution-quotation/agents/openai.yaml +4 -4
- package/skills/microi-solution-quotation/scripts/build_solution_quote.py +296 -296
- package/skills/microi-system-delivery/SKILL.md +16 -4
- package/skills/microi-system-delivery/references/progressive-01-/346/240/207/345/207/206/345/267/245/344/275/234/346/265/201.md +39 -1
- package/skills/microi-system-delivery/references/progressive-02-/350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/345/277/205/351/241/273/350/246/206/347/233/226/347/232/204/345/235/221.md +3 -3
- package/skills/microi.v8.js +1921 -1921
- package/skills/module-engine/SKILL.md +26 -0
- package/skills/ocr-engine/agents/openai.yaml +4 -4
- package/skills/page-engine/SKILL.md +21 -0
- package/skills/page-engine/examples/compact-dashboard.json +1444 -1444
- package/skills/playwright-e2e/references/progressive-03-microi-helper-/346/250/241/346/235/277.md +3 -3
- package/skills/report-engine/SKILL.md +12 -0
- package/skills/report-engine/agents/openai.yaml +4 -4
- package/skills/scripts/optimize-progressive-disclosure.mjs +204 -204
- package/skills/scripts/refresh-progressive-disclosure.mjs +64 -64
- package/skills/scripts/sync-embedded-skills.mjs +46 -46
- package/skills/scripts/validate-progressive-disclosure.mjs +57 -57
- package/skills/search-engine/agents/openai.yaml +4 -4
- package/skills/translate-engine/agents/openai.yaml +4 -4
- package/skills/ui-design/assets/pattern-showcase/app.js +54 -54
- package/skills/ui-design/assets/pattern-showcase/index.html +163 -163
- package/skills/ui-design/assets/pattern-showcase/styles.css +311 -311
- package/skills/unity-integration/agents/openai.yaml +4 -4
- package/skills/v8-image-processing/agents/openai.yaml +4 -4
- package/skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs +203 -203
- package/skills/v8-tcp-integration/agents/openai.yaml +4 -4
- package/skills/vision-engine/agents/openai.yaml +4 -4
- package/skills/workspace-conventions/SKILL.md +2 -2
package/skills/playwright-e2e/references/progressive-03-microi-helper-/346/250/241/346/235/277.md
CHANGED
|
@@ -134,7 +134,7 @@ test('公开接口引擎返回标准 DosResult', async ({ request }) => {
|
|
|
134
134
|
6. 退出登录能清理 Token 并回到登录页。
|
|
135
135
|
|
|
136
136
|
<!-- /microi-progressive:chunk -->
|
|
137
|
-
<!-- microi-progressive:chunk id=playwright-e2e-020 sha256=
|
|
137
|
+
<!-- microi-progressive:chunk id=playwright-e2e-020 sha256=63b5c2374f7e011a927ebd751d01c50e2d54d9e746d4d47ddd1665779a4487b9 -->
|
|
138
138
|
## 完整业务验收门槛
|
|
139
139
|
|
|
140
140
|
当用户要求“完整测试”“全面测试”“不要让我手工测出接口 null/404/权限漏洞”时,不能只生成浅冒烟。至少补齐以下测试文件:
|
|
@@ -157,9 +157,9 @@ tests/e2e/
|
|
|
157
157
|
4. UI 点击不能只断言 toast;必须用 `page.waitForResponse()` 捕捉对应接口,并验证响应体。
|
|
158
158
|
5. 任何 FormEngine HTTP 路由必须使用 `/api/formengine/{action}-{table}`,不要生成 `/formengine/{table}/{action}`。
|
|
159
159
|
6. ApiEngine 使用稳定路径 `/apiengine/{ApiEngineKey}`,并通过唯一的 `osclient` Header 传租户;JSON/Form Body 可冗余携带 `OsClient`。普通 POST/PUT/PATCH/DELETE 禁止追加 `--OsClient--...--`。仅第三方回调或无法设置 Header/Form/Query 的 GET/HEAD 才允许特殊路径。
|
|
160
|
-
7.
|
|
160
|
+
7. 最终业务页面级测试必须把 `请求失败`、`网络错误`、`null`、`待开发`、`开发中` 当成失败信号;最终菜单入口或业务 toast 出现占位文案仍表示未交付。菜单骨架阶段另用专门用例验收共享开发中页的菜单身份、布局、动画和权限,该阶段允许预期占位文案,但不算业务功能交付通过,不能套用此例外放过最终业务页面。
|
|
161
161
|
8. 新增、保存、结算、转赠、确认、上传等按钮不能只弹“成功/待开发”toast;必须调用真实业务接口,并通过后端查询验证状态变化。
|
|
162
|
-
9.
|
|
162
|
+
9. 源码守卫可作为兜底:递归扫描最终业务源码 `src/**/*.vue`、`src/**/*.js`,禁止残留 `待开发|开发中` 占位文案。平台共享 `Microi.Client/src/views/ai-engine/developing.vue` 及其专门占位验收用例可以按已核验的精确文件加入白名单;不得排除整个 `ai-engine`、`src` 或业务目录,也不得用源码白名单豁免最终菜单仍指向占位页的失败。
|
|
163
163
|
10. 新建或更新 ApiEngine 后必须用 HTTP 路径复测一次;只用 `microi_run_engine` 通过不够,因为 HTTP 调用还受 `IsEnable`、`StopHttp`、`AllowAnonymous` 和动态路由缓存影响。
|
|
164
164
|
11. 每个移动端项目都要有“接口清单驱动”的契约测试:静态扫描或维护清单覆盖所有 `callEngine('xxx')`,逐个 HTTP 调用并断言不是 404、不是空响应、不是字符串 `null`,且必须是标准 DosResult。
|
|
165
165
|
12. 写业务闭环时必须准备可重复测试数据并清理:例如抢购要“创建测试挂单 → 调用抢购 → 验证订单 → 删除测试订单/挂单/提货卡”,购物车要“加入购物车 → 结算 → 验证订单/购物车状态 → 删除测试订单”。
|
|
@@ -11,6 +11,12 @@ description: Microi 报表引擎设计与验收规范。用于 Rpt_Report 虚拟
|
|
|
11
11
|
|
|
12
12
|
报表引擎以 `Rpt_Report`、报表字段配置和数据源引擎组成虚拟表格。表格展示使用报表引擎;ECharts 图表和仪表盘使用界面引擎。报表字段是虚拟字段,不能假设存在同名物理表列。
|
|
13
13
|
|
|
14
|
+
完整 AI 系统开发默认必须交付需求相关的业务统计、老板经营、领导管理报表。
|
|
15
|
+
这些决策看板基于界面引擎,读取 `../page-engine/SKILL.md`;本报表引擎负责其分页明细、
|
|
16
|
+
查询汇总表与导出,不能用一个虚拟表格或列表角标代替全部看板。菜单骨架、占位切换和指标
|
|
17
|
+
范围统一读取[菜单先行与经营报表增补要求](../microi-system-delivery/references/progressive-01-标准工作流.md#菜单先行与经营报表增补要求强制)。
|
|
18
|
+
局部修改限相关报表,只读分析不写资源;完整开发不等于生产写入授权。
|
|
19
|
+
|
|
14
20
|
## 建模顺序
|
|
15
21
|
|
|
16
22
|
1. 明确指标口径、时间粒度、维度、权限和导出上限。
|
|
@@ -60,6 +66,11 @@ var result = await V8.ApiEngine.Run('report_adjust_inventory', {
|
|
|
60
66
|
|
|
61
67
|
界面引擎图表应调用返回稳定 `{Code, Data}` 的统计接口引擎,不在前端下载明细再聚合。统计接口设置时间范围、维度和最大点数,返回空数据时保持稳定结构。
|
|
62
68
|
|
|
69
|
+
与看板配套的明细/导出必须共享指标公式、业务状态、时间字段/时区/区间边界、比较基期和
|
|
70
|
+
部门/人员/项目等筛选条件。聚合与穿透明细分别执行当前租户和用户数据权限,不能信任前端
|
|
71
|
+
带来的老板/领导角色或任意字段名。真实零值、没有数据、未配置来源与查询失败分别表达;
|
|
72
|
+
不能把失败转成成功零值。用同一筛选的明细合计独立核对看板数值,确无明细的汇总注明限制。
|
|
73
|
+
|
|
63
74
|
## 验收清单
|
|
64
75
|
|
|
65
76
|
- [ ] 指标口径、时区和小数精度明确
|
|
@@ -69,3 +80,4 @@ var result = await V8.ApiEngine.Run('report_adjust_inventory', {
|
|
|
69
80
|
- [ ] 分页、聚合、导出均有资源上限
|
|
70
81
|
- [ ] 缓存按租户和权限隔离
|
|
71
82
|
- [ ] 普通角色、无权限角色、跨租户负向测试通过
|
|
83
|
+
- [ ] 配套界面引擎看板与穿透明细/导出使用同一口径,时间边界、筛选、零/空/失败及明细对账通过
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "报表引擎"
|
|
3
|
-
short_description: "设计遵守菜单与行级权限的报表、聚合接口、写操作与导出验收"
|
|
4
|
-
default_prompt: "使用 $report-engine 设计并验收当前 Microi 报表。"
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "报表引擎"
|
|
3
|
+
short_description: "设计遵守菜单与行级权限的报表、聚合接口、写操作与导出验收"
|
|
4
|
+
default_prompt: "使用 $report-engine 设计并验收当前 Microi 报表。"
|
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
import crypto from 'node:crypto'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
|
|
6
|
-
const skillsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
-
const manifestPath = path.join(skillsRoot, '.progressive-disclosure-manifest.json')
|
|
8
|
-
const maxEntryLines = 285
|
|
9
|
-
const keepBudgetLines = 170
|
|
10
|
-
const referenceBudgetLines = 245
|
|
11
|
-
|
|
12
|
-
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex')
|
|
13
|
-
const lineCount = (value) => value.length === 0 ? 0 : value.split(/\r?\n/).length
|
|
14
|
-
const slug = (value) => value
|
|
15
|
-
.replace(/^#+\s*/, '')
|
|
16
|
-
.replace(/[`*_]/g, '')
|
|
17
|
-
.trim()
|
|
18
|
-
.toLowerCase()
|
|
19
|
-
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
20
|
-
.replace(/^-|-$/g, '')
|
|
21
|
-
.slice(0, 48) || 'reference'
|
|
22
|
-
|
|
23
|
-
const splitAtHeading = (value, headingPattern) => {
|
|
24
|
-
const matches = [...value.matchAll(headingPattern)]
|
|
25
|
-
if (matches.length <= 1) return [value]
|
|
26
|
-
const parts = []
|
|
27
|
-
if (matches[0].index > 0) parts.push(value.slice(0, matches[0].index))
|
|
28
|
-
for (let index = 0; index < matches.length; index += 1) {
|
|
29
|
-
parts.push(value.slice(matches[index].index, matches[index + 1]?.index ?? value.length))
|
|
30
|
-
}
|
|
31
|
-
return parts.filter(Boolean)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const splitOversized = (value, eol) => {
|
|
35
|
-
if (lineCount(value) <= referenceBudgetLines) return [value]
|
|
36
|
-
let parts = splitAtHeading(value, /^###\s+.+$/gm)
|
|
37
|
-
if (parts.length === 1) parts = splitAtHeading(value, /^####\s+.+$/gm)
|
|
38
|
-
const output = []
|
|
39
|
-
for (const part of parts) {
|
|
40
|
-
if (lineCount(part) <= referenceBudgetLines) {
|
|
41
|
-
output.push(part)
|
|
42
|
-
continue
|
|
43
|
-
}
|
|
44
|
-
const lines = part.split(/\r?\n/)
|
|
45
|
-
let start = 0
|
|
46
|
-
while (start < lines.length) {
|
|
47
|
-
let end = Math.min(start + referenceBudgetLines - 8, lines.length)
|
|
48
|
-
if (end < lines.length) {
|
|
49
|
-
for (let cursor = end; cursor > start + 40; cursor -= 1) {
|
|
50
|
-
if (lines[cursor - 1].trim() === '') {
|
|
51
|
-
end = cursor
|
|
52
|
-
break
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const hasTrailingBreak = end < lines.length || part.endsWith(eol)
|
|
57
|
-
output.push(lines.slice(start, end).join(eol) + (hasTrailingBreak ? eol : ''))
|
|
58
|
-
start = end
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return output
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const headingOf = (value, fallback) => value.match(/^#{2,4}\s+(.+)$/m)?.[1]?.trim() || fallback
|
|
65
|
-
|
|
66
|
-
const skillDirectories = fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
67
|
-
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(skillsRoot, entry.name, 'SKILL.md')))
|
|
68
|
-
.map((entry) => entry.name)
|
|
69
|
-
.sort()
|
|
70
|
-
|
|
71
|
-
const manifest = {
|
|
72
|
-
version: 1,
|
|
73
|
-
generatedAt: new Date().toISOString(),
|
|
74
|
-
policy: {
|
|
75
|
-
maxEntryLines,
|
|
76
|
-
referenceBudgetLines,
|
|
77
|
-
note: 'Original prefix and ordered chunks reconstruct the exact pre-split SKILL.md bytes.'
|
|
78
|
-
},
|
|
79
|
-
skills: {}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const skillName of skillDirectories) {
|
|
83
|
-
const skillPath = path.join(skillsRoot, skillName, 'SKILL.md')
|
|
84
|
-
const original = fs.readFileSync(skillPath, 'utf8')
|
|
85
|
-
if (lineCount(original) <= 300) continue
|
|
86
|
-
if (original.includes('<!-- microi-progressive:begin -->')) {
|
|
87
|
-
throw new Error(`${skillName} is already optimized; validate instead of running twice`)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const eol = original.includes('\r\n') ? '\r\n' : '\n'
|
|
91
|
-
const sectionMatches = [...original.matchAll(/^##\s+.+$/gm)]
|
|
92
|
-
if (sectionMatches.length < 2) {
|
|
93
|
-
throw new Error(`${skillName} needs at least two level-2 sections for safe semantic splitting`)
|
|
94
|
-
}
|
|
95
|
-
const prefix = original.slice(0, sectionMatches[0].index)
|
|
96
|
-
const sections = sectionMatches.map((match, index) => ({
|
|
97
|
-
heading: match[0].replace(/^##\s+/, '').trim(),
|
|
98
|
-
raw: original.slice(match.index, sectionMatches[index + 1]?.index ?? original.length),
|
|
99
|
-
}))
|
|
100
|
-
|
|
101
|
-
const kept = []
|
|
102
|
-
const moved = []
|
|
103
|
-
let keptLines = lineCount(prefix)
|
|
104
|
-
for (const section of sections) {
|
|
105
|
-
// Keep one contiguous prefix only. Picking a later short section after an
|
|
106
|
-
// earlier section was moved would reorder the original knowledge stream.
|
|
107
|
-
if (moved.length === 0 && (kept.length === 0 || keptLines + lineCount(section.raw) <= keepBudgetLines)) {
|
|
108
|
-
kept.push(section)
|
|
109
|
-
keptLines += lineCount(section.raw)
|
|
110
|
-
} else {
|
|
111
|
-
moved.push(section)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (moved.length === 0) {
|
|
115
|
-
moved.push(kept.pop())
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const fragments = moved.flatMap((section) => splitOversized(section.raw, eol).map((raw, index) => ({
|
|
119
|
-
raw,
|
|
120
|
-
heading: headingOf(raw, `${section.heading} (${index + 1})`),
|
|
121
|
-
})))
|
|
122
|
-
|
|
123
|
-
const referenceGroups = []
|
|
124
|
-
let current = []
|
|
125
|
-
let currentLines = 5
|
|
126
|
-
for (const fragment of fragments) {
|
|
127
|
-
const cost = lineCount(fragment.raw) + 3
|
|
128
|
-
if (current.length && currentLines + cost > referenceBudgetLines) {
|
|
129
|
-
referenceGroups.push(current)
|
|
130
|
-
current = []
|
|
131
|
-
currentLines = 5
|
|
132
|
-
}
|
|
133
|
-
current.push(fragment)
|
|
134
|
-
currentLines += cost
|
|
135
|
-
}
|
|
136
|
-
if (current.length) referenceGroups.push(current)
|
|
137
|
-
|
|
138
|
-
const referencesDirectory = path.join(skillsRoot, skillName, 'references')
|
|
139
|
-
fs.mkdirSync(referencesDirectory, { recursive: true })
|
|
140
|
-
const chunkRecords = []
|
|
141
|
-
let ordinal = 0
|
|
142
|
-
const wrapChunk = (raw, destination) => {
|
|
143
|
-
const id = `${skillName}-${String(ordinal).padStart(3, '0')}`
|
|
144
|
-
const record = {
|
|
145
|
-
id,
|
|
146
|
-
ordinal,
|
|
147
|
-
destination: destination.replaceAll('\\', '/'),
|
|
148
|
-
sha256: hash(raw),
|
|
149
|
-
lines: lineCount(raw),
|
|
150
|
-
heading: headingOf(raw, skillName),
|
|
151
|
-
}
|
|
152
|
-
chunkRecords.push(record)
|
|
153
|
-
ordinal += 1
|
|
154
|
-
return `<!-- microi-progressive:chunk id=${id} sha256=${record.sha256} -->${eol}${raw}<!-- /microi-progressive:chunk -->${eol}`
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const entryChunks = kept.map((section) => wrapChunk(section.raw, `${skillName}/SKILL.md`)).join('')
|
|
158
|
-
const referenceRecords = []
|
|
159
|
-
referenceGroups.forEach((group, index) => {
|
|
160
|
-
const fileName = `progressive-${String(index + 1).padStart(2, '0')}-${slug(group[0].heading)}.md`
|
|
161
|
-
const relative = `references/${fileName}`
|
|
162
|
-
const destination = `${skillName}/${relative}`
|
|
163
|
-
const body = group.map((fragment) => wrapChunk(fragment.raw, destination)).join('')
|
|
164
|
-
const title = `# ${skillName} 详细参考 ${index + 1}${eol}${eol}`
|
|
165
|
-
const note = `> 按需读取;本文件由 SKILL.md 的原章节无损拆分。${eol}${eol}`
|
|
166
|
-
fs.writeFileSync(path.join(referencesDirectory, fileName), title + note + body, 'utf8')
|
|
167
|
-
referenceRecords.push({
|
|
168
|
-
path: relative,
|
|
169
|
-
headings: group.map((fragment) => fragment.heading),
|
|
170
|
-
})
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
const routerLines = [
|
|
174
|
-
'## 详细参考路由(渐进披露)',
|
|
175
|
-
'',
|
|
176
|
-
'仅在当前任务涉及对应主题时读取;下列文件合计保留了原 SKILL.md 的全部详细知识。',
|
|
177
|
-
'',
|
|
178
|
-
...referenceRecords.map((reference) =>
|
|
179
|
-
`- [${reference.path}](${reference.path}):${reference.headings.join(';')}`),
|
|
180
|
-
'',
|
|
181
|
-
].join(eol)
|
|
182
|
-
const rewritten = prefix
|
|
183
|
-
+ `<!-- microi-progressive:begin -->${eol}`
|
|
184
|
-
+ entryChunks
|
|
185
|
-
+ routerLines
|
|
186
|
-
+ `<!-- microi-progressive:end -->${eol}`
|
|
187
|
-
if (lineCount(rewritten) > maxEntryLines) {
|
|
188
|
-
throw new Error(`${skillName} entry remains ${lineCount(rewritten)} lines; lower keepBudgetLines`)
|
|
189
|
-
}
|
|
190
|
-
fs.writeFileSync(skillPath, rewritten, 'utf8')
|
|
191
|
-
|
|
192
|
-
manifest.skills[skillName] = {
|
|
193
|
-
originalSha256: hash(original),
|
|
194
|
-
originalLines: lineCount(original),
|
|
195
|
-
prefixSha256: hash(prefix),
|
|
196
|
-
prefixBytes: Buffer.byteLength(prefix),
|
|
197
|
-
entryLines: lineCount(rewritten),
|
|
198
|
-
chunks: chunkRecords,
|
|
199
|
-
references: referenceRecords,
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
204
|
-
console.log(`Optimized ${Object.keys(manifest.skills).length} skills; manifest: ${manifestPath}`)
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const skillsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
+
const manifestPath = path.join(skillsRoot, '.progressive-disclosure-manifest.json')
|
|
8
|
+
const maxEntryLines = 285
|
|
9
|
+
const keepBudgetLines = 170
|
|
10
|
+
const referenceBudgetLines = 245
|
|
11
|
+
|
|
12
|
+
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex')
|
|
13
|
+
const lineCount = (value) => value.length === 0 ? 0 : value.split(/\r?\n/).length
|
|
14
|
+
const slug = (value) => value
|
|
15
|
+
.replace(/^#+\s*/, '')
|
|
16
|
+
.replace(/[`*_]/g, '')
|
|
17
|
+
.trim()
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
20
|
+
.replace(/^-|-$/g, '')
|
|
21
|
+
.slice(0, 48) || 'reference'
|
|
22
|
+
|
|
23
|
+
const splitAtHeading = (value, headingPattern) => {
|
|
24
|
+
const matches = [...value.matchAll(headingPattern)]
|
|
25
|
+
if (matches.length <= 1) return [value]
|
|
26
|
+
const parts = []
|
|
27
|
+
if (matches[0].index > 0) parts.push(value.slice(0, matches[0].index))
|
|
28
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
29
|
+
parts.push(value.slice(matches[index].index, matches[index + 1]?.index ?? value.length))
|
|
30
|
+
}
|
|
31
|
+
return parts.filter(Boolean)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const splitOversized = (value, eol) => {
|
|
35
|
+
if (lineCount(value) <= referenceBudgetLines) return [value]
|
|
36
|
+
let parts = splitAtHeading(value, /^###\s+.+$/gm)
|
|
37
|
+
if (parts.length === 1) parts = splitAtHeading(value, /^####\s+.+$/gm)
|
|
38
|
+
const output = []
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (lineCount(part) <= referenceBudgetLines) {
|
|
41
|
+
output.push(part)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
const lines = part.split(/\r?\n/)
|
|
45
|
+
let start = 0
|
|
46
|
+
while (start < lines.length) {
|
|
47
|
+
let end = Math.min(start + referenceBudgetLines - 8, lines.length)
|
|
48
|
+
if (end < lines.length) {
|
|
49
|
+
for (let cursor = end; cursor > start + 40; cursor -= 1) {
|
|
50
|
+
if (lines[cursor - 1].trim() === '') {
|
|
51
|
+
end = cursor
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const hasTrailingBreak = end < lines.length || part.endsWith(eol)
|
|
57
|
+
output.push(lines.slice(start, end).join(eol) + (hasTrailingBreak ? eol : ''))
|
|
58
|
+
start = end
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return output
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headingOf = (value, fallback) => value.match(/^#{2,4}\s+(.+)$/m)?.[1]?.trim() || fallback
|
|
65
|
+
|
|
66
|
+
const skillDirectories = fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
67
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(skillsRoot, entry.name, 'SKILL.md')))
|
|
68
|
+
.map((entry) => entry.name)
|
|
69
|
+
.sort()
|
|
70
|
+
|
|
71
|
+
const manifest = {
|
|
72
|
+
version: 1,
|
|
73
|
+
generatedAt: new Date().toISOString(),
|
|
74
|
+
policy: {
|
|
75
|
+
maxEntryLines,
|
|
76
|
+
referenceBudgetLines,
|
|
77
|
+
note: 'Original prefix and ordered chunks reconstruct the exact pre-split SKILL.md bytes.'
|
|
78
|
+
},
|
|
79
|
+
skills: {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const skillName of skillDirectories) {
|
|
83
|
+
const skillPath = path.join(skillsRoot, skillName, 'SKILL.md')
|
|
84
|
+
const original = fs.readFileSync(skillPath, 'utf8')
|
|
85
|
+
if (lineCount(original) <= 300) continue
|
|
86
|
+
if (original.includes('<!-- microi-progressive:begin -->')) {
|
|
87
|
+
throw new Error(`${skillName} is already optimized; validate instead of running twice`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const eol = original.includes('\r\n') ? '\r\n' : '\n'
|
|
91
|
+
const sectionMatches = [...original.matchAll(/^##\s+.+$/gm)]
|
|
92
|
+
if (sectionMatches.length < 2) {
|
|
93
|
+
throw new Error(`${skillName} needs at least two level-2 sections for safe semantic splitting`)
|
|
94
|
+
}
|
|
95
|
+
const prefix = original.slice(0, sectionMatches[0].index)
|
|
96
|
+
const sections = sectionMatches.map((match, index) => ({
|
|
97
|
+
heading: match[0].replace(/^##\s+/, '').trim(),
|
|
98
|
+
raw: original.slice(match.index, sectionMatches[index + 1]?.index ?? original.length),
|
|
99
|
+
}))
|
|
100
|
+
|
|
101
|
+
const kept = []
|
|
102
|
+
const moved = []
|
|
103
|
+
let keptLines = lineCount(prefix)
|
|
104
|
+
for (const section of sections) {
|
|
105
|
+
// Keep one contiguous prefix only. Picking a later short section after an
|
|
106
|
+
// earlier section was moved would reorder the original knowledge stream.
|
|
107
|
+
if (moved.length === 0 && (kept.length === 0 || keptLines + lineCount(section.raw) <= keepBudgetLines)) {
|
|
108
|
+
kept.push(section)
|
|
109
|
+
keptLines += lineCount(section.raw)
|
|
110
|
+
} else {
|
|
111
|
+
moved.push(section)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (moved.length === 0) {
|
|
115
|
+
moved.push(kept.pop())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fragments = moved.flatMap((section) => splitOversized(section.raw, eol).map((raw, index) => ({
|
|
119
|
+
raw,
|
|
120
|
+
heading: headingOf(raw, `${section.heading} (${index + 1})`),
|
|
121
|
+
})))
|
|
122
|
+
|
|
123
|
+
const referenceGroups = []
|
|
124
|
+
let current = []
|
|
125
|
+
let currentLines = 5
|
|
126
|
+
for (const fragment of fragments) {
|
|
127
|
+
const cost = lineCount(fragment.raw) + 3
|
|
128
|
+
if (current.length && currentLines + cost > referenceBudgetLines) {
|
|
129
|
+
referenceGroups.push(current)
|
|
130
|
+
current = []
|
|
131
|
+
currentLines = 5
|
|
132
|
+
}
|
|
133
|
+
current.push(fragment)
|
|
134
|
+
currentLines += cost
|
|
135
|
+
}
|
|
136
|
+
if (current.length) referenceGroups.push(current)
|
|
137
|
+
|
|
138
|
+
const referencesDirectory = path.join(skillsRoot, skillName, 'references')
|
|
139
|
+
fs.mkdirSync(referencesDirectory, { recursive: true })
|
|
140
|
+
const chunkRecords = []
|
|
141
|
+
let ordinal = 0
|
|
142
|
+
const wrapChunk = (raw, destination) => {
|
|
143
|
+
const id = `${skillName}-${String(ordinal).padStart(3, '0')}`
|
|
144
|
+
const record = {
|
|
145
|
+
id,
|
|
146
|
+
ordinal,
|
|
147
|
+
destination: destination.replaceAll('\\', '/'),
|
|
148
|
+
sha256: hash(raw),
|
|
149
|
+
lines: lineCount(raw),
|
|
150
|
+
heading: headingOf(raw, skillName),
|
|
151
|
+
}
|
|
152
|
+
chunkRecords.push(record)
|
|
153
|
+
ordinal += 1
|
|
154
|
+
return `<!-- microi-progressive:chunk id=${id} sha256=${record.sha256} -->${eol}${raw}<!-- /microi-progressive:chunk -->${eol}`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const entryChunks = kept.map((section) => wrapChunk(section.raw, `${skillName}/SKILL.md`)).join('')
|
|
158
|
+
const referenceRecords = []
|
|
159
|
+
referenceGroups.forEach((group, index) => {
|
|
160
|
+
const fileName = `progressive-${String(index + 1).padStart(2, '0')}-${slug(group[0].heading)}.md`
|
|
161
|
+
const relative = `references/${fileName}`
|
|
162
|
+
const destination = `${skillName}/${relative}`
|
|
163
|
+
const body = group.map((fragment) => wrapChunk(fragment.raw, destination)).join('')
|
|
164
|
+
const title = `# ${skillName} 详细参考 ${index + 1}${eol}${eol}`
|
|
165
|
+
const note = `> 按需读取;本文件由 SKILL.md 的原章节无损拆分。${eol}${eol}`
|
|
166
|
+
fs.writeFileSync(path.join(referencesDirectory, fileName), title + note + body, 'utf8')
|
|
167
|
+
referenceRecords.push({
|
|
168
|
+
path: relative,
|
|
169
|
+
headings: group.map((fragment) => fragment.heading),
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const routerLines = [
|
|
174
|
+
'## 详细参考路由(渐进披露)',
|
|
175
|
+
'',
|
|
176
|
+
'仅在当前任务涉及对应主题时读取;下列文件合计保留了原 SKILL.md 的全部详细知识。',
|
|
177
|
+
'',
|
|
178
|
+
...referenceRecords.map((reference) =>
|
|
179
|
+
`- [${reference.path}](${reference.path}):${reference.headings.join(';')}`),
|
|
180
|
+
'',
|
|
181
|
+
].join(eol)
|
|
182
|
+
const rewritten = prefix
|
|
183
|
+
+ `<!-- microi-progressive:begin -->${eol}`
|
|
184
|
+
+ entryChunks
|
|
185
|
+
+ routerLines
|
|
186
|
+
+ `<!-- microi-progressive:end -->${eol}`
|
|
187
|
+
if (lineCount(rewritten) > maxEntryLines) {
|
|
188
|
+
throw new Error(`${skillName} entry remains ${lineCount(rewritten)} lines; lower keepBudgetLines`)
|
|
189
|
+
}
|
|
190
|
+
fs.writeFileSync(skillPath, rewritten, 'utf8')
|
|
191
|
+
|
|
192
|
+
manifest.skills[skillName] = {
|
|
193
|
+
originalSha256: hash(original),
|
|
194
|
+
originalLines: lineCount(original),
|
|
195
|
+
prefixSha256: hash(prefix),
|
|
196
|
+
prefixBytes: Buffer.byteLength(prefix),
|
|
197
|
+
entryLines: lineCount(rewritten),
|
|
198
|
+
chunks: chunkRecords,
|
|
199
|
+
references: referenceRecords,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
204
|
+
console.log(`Optimized ${Object.keys(manifest.skills).length} skills; manifest: ${manifestPath}`)
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import crypto from 'node:crypto'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
|
|
6
|
-
const skillsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
-
const manifestPath = path.join(skillsRoot, '.progressive-disclosure-manifest.json')
|
|
8
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
9
|
-
const requested = process.argv.slice(2)
|
|
10
|
-
if (requested.length === 0) {
|
|
11
|
-
throw new Error('Pass each intentionally changed skill name explicitly.')
|
|
12
|
-
}
|
|
13
|
-
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex')
|
|
14
|
-
|
|
15
|
-
for (const skillName of requested) {
|
|
16
|
-
const skill = manifest.skills[skillName]
|
|
17
|
-
if (!skill) throw new Error(`${skillName} is not a progressive-disclosure skill`)
|
|
18
|
-
const sources = new Map()
|
|
19
|
-
const entryPath = path.join(skillsRoot, skillName, 'SKILL.md')
|
|
20
|
-
sources.set(`${skillName}/SKILL.md`, fs.readFileSync(entryPath, 'utf8'))
|
|
21
|
-
for (const reference of skill.references) {
|
|
22
|
-
const destination = `${skillName}/${reference.path}`
|
|
23
|
-
sources.set(destination, fs.readFileSync(path.join(skillsRoot, destination), 'utf8'))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const ordered = []
|
|
27
|
-
for (const chunk of skill.chunks) {
|
|
28
|
-
let source = sources.get(chunk.destination)
|
|
29
|
-
const openPattern = new RegExp(`<!-- microi-progressive:chunk id=${chunk.id} sha256=[a-f0-9]{64} -->`)
|
|
30
|
-
const match = source.match(openPattern)
|
|
31
|
-
if (!match) throw new Error(`${skillName}: missing chunk ${chunk.id}`)
|
|
32
|
-
const markerStart = match.index
|
|
33
|
-
const markerEnd = markerStart + match[0].length
|
|
34
|
-
const contentStart = markerEnd + (source.startsWith('\r\n', markerEnd) ? 2 : 1)
|
|
35
|
-
const close = '<!-- /microi-progressive:chunk -->'
|
|
36
|
-
const contentEnd = source.indexOf(close, contentStart)
|
|
37
|
-
const raw = source.slice(contentStart, contentEnd)
|
|
38
|
-
const digest = hash(raw)
|
|
39
|
-
source = source.slice(0, markerStart)
|
|
40
|
-
+ `<!-- microi-progressive:chunk id=${chunk.id} sha256=${digest} -->`
|
|
41
|
-
+ source.slice(markerEnd)
|
|
42
|
-
sources.set(chunk.destination, source)
|
|
43
|
-
chunk.sha256 = digest
|
|
44
|
-
chunk.lines = raw.split(/\r?\n/).length
|
|
45
|
-
ordered[chunk.ordinal] = raw
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
for (const [destination, source] of sources) {
|
|
49
|
-
fs.writeFileSync(path.join(skillsRoot, destination), source, 'utf8')
|
|
50
|
-
}
|
|
51
|
-
const entry = sources.get(`${skillName}/SKILL.md`)
|
|
52
|
-
const begin = entry.indexOf('<!-- microi-progressive:begin -->')
|
|
53
|
-
const prefix = entry.slice(0, begin)
|
|
54
|
-
skill.prefixSha256 = hash(prefix)
|
|
55
|
-
skill.prefixBytes = Buffer.byteLength(prefix)
|
|
56
|
-
skill.originalSha256 = hash(prefix + ordered.join(''))
|
|
57
|
-
skill.originalLines = (prefix + ordered.join('')).split(/\r?\n/).length
|
|
58
|
-
skill.entryLines = entry.split(/\r?\n/).length
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
manifest.generatedAt = new Date().toISOString()
|
|
62
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
63
|
-
console.log(`Accepted intentional knowledge updates for: ${requested.join(', ')}`)
|
|
64
|
-
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const skillsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
+
const manifestPath = path.join(skillsRoot, '.progressive-disclosure-manifest.json')
|
|
8
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
9
|
+
const requested = process.argv.slice(2)
|
|
10
|
+
if (requested.length === 0) {
|
|
11
|
+
throw new Error('Pass each intentionally changed skill name explicitly.')
|
|
12
|
+
}
|
|
13
|
+
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex')
|
|
14
|
+
|
|
15
|
+
for (const skillName of requested) {
|
|
16
|
+
const skill = manifest.skills[skillName]
|
|
17
|
+
if (!skill) throw new Error(`${skillName} is not a progressive-disclosure skill`)
|
|
18
|
+
const sources = new Map()
|
|
19
|
+
const entryPath = path.join(skillsRoot, skillName, 'SKILL.md')
|
|
20
|
+
sources.set(`${skillName}/SKILL.md`, fs.readFileSync(entryPath, 'utf8'))
|
|
21
|
+
for (const reference of skill.references) {
|
|
22
|
+
const destination = `${skillName}/${reference.path}`
|
|
23
|
+
sources.set(destination, fs.readFileSync(path.join(skillsRoot, destination), 'utf8'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ordered = []
|
|
27
|
+
for (const chunk of skill.chunks) {
|
|
28
|
+
let source = sources.get(chunk.destination)
|
|
29
|
+
const openPattern = new RegExp(`<!-- microi-progressive:chunk id=${chunk.id} sha256=[a-f0-9]{64} -->`)
|
|
30
|
+
const match = source.match(openPattern)
|
|
31
|
+
if (!match) throw new Error(`${skillName}: missing chunk ${chunk.id}`)
|
|
32
|
+
const markerStart = match.index
|
|
33
|
+
const markerEnd = markerStart + match[0].length
|
|
34
|
+
const contentStart = markerEnd + (source.startsWith('\r\n', markerEnd) ? 2 : 1)
|
|
35
|
+
const close = '<!-- /microi-progressive:chunk -->'
|
|
36
|
+
const contentEnd = source.indexOf(close, contentStart)
|
|
37
|
+
const raw = source.slice(contentStart, contentEnd)
|
|
38
|
+
const digest = hash(raw)
|
|
39
|
+
source = source.slice(0, markerStart)
|
|
40
|
+
+ `<!-- microi-progressive:chunk id=${chunk.id} sha256=${digest} -->`
|
|
41
|
+
+ source.slice(markerEnd)
|
|
42
|
+
sources.set(chunk.destination, source)
|
|
43
|
+
chunk.sha256 = digest
|
|
44
|
+
chunk.lines = raw.split(/\r?\n/).length
|
|
45
|
+
ordered[chunk.ordinal] = raw
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [destination, source] of sources) {
|
|
49
|
+
fs.writeFileSync(path.join(skillsRoot, destination), source, 'utf8')
|
|
50
|
+
}
|
|
51
|
+
const entry = sources.get(`${skillName}/SKILL.md`)
|
|
52
|
+
const begin = entry.indexOf('<!-- microi-progressive:begin -->')
|
|
53
|
+
const prefix = entry.slice(0, begin)
|
|
54
|
+
skill.prefixSha256 = hash(prefix)
|
|
55
|
+
skill.prefixBytes = Buffer.byteLength(prefix)
|
|
56
|
+
skill.originalSha256 = hash(prefix + ordered.join(''))
|
|
57
|
+
skill.originalLines = (prefix + ordered.join('')).split(/\r?\n/).length
|
|
58
|
+
skill.entryLines = entry.split(/\r?\n/).length
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
manifest.generatedAt = new Date().toISOString()
|
|
62
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
63
|
+
console.log(`Accepted intentional knowledge updates for: ${requested.join(', ')}`)
|
|
64
|
+
|