@liduchuan/atlas-loop 0.0.0-stage → 0.1.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.
- package/README.md +112 -2
- package/SKILL.md +61 -0
- package/assets/viewer-shell.html +1911 -0
- package/bin/atlas-loop.mjs +99 -0
- package/package.json +52 -4
- package/references/schema.md +122 -0
- package/scripts/build_atlas.mjs +261 -0
- package/scripts/verify_atlas.mjs +316 -0
- package/template/app.jsx +1339 -0
- package/template/build-shell.mjs +82 -0
- package/template/dev-server.mjs +177 -0
- package/template/sample-data.json +253 -0
- package/template/styles.css +1188 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
9
|
+
|
|
10
|
+
function ensureDevDeps() {
|
|
11
|
+
const rfCss = path.join(REPO_ROOT, 'node_modules', '@xyflow', 'react', 'dist', 'style.css');
|
|
12
|
+
if (fs.existsSync(rfCss)) return;
|
|
13
|
+
try {
|
|
14
|
+
execSync('bun install', { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
15
|
+
} catch (_) {
|
|
16
|
+
execSync('npm install', { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function compileBundle() {
|
|
21
|
+
const entry = path.join(__dirname, 'app.jsx');
|
|
22
|
+
const esbuildPath = path.join(REPO_ROOT, 'node_modules', 'esbuild', 'lib', 'main.js');
|
|
23
|
+
if (fs.existsSync(esbuildPath)) {
|
|
24
|
+
const esbuild = await import(esbuildPath);
|
|
25
|
+
const res = await esbuild.build({
|
|
26
|
+
entryPoints: [entry],
|
|
27
|
+
bundle: true,
|
|
28
|
+
minify: true,
|
|
29
|
+
write: false,
|
|
30
|
+
format: 'iife',
|
|
31
|
+
target: ['es2020'],
|
|
32
|
+
jsx: 'automatic',
|
|
33
|
+
define: {
|
|
34
|
+
'process.env.NODE_ENV': '"production"',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return res.outputFiles[0].text;
|
|
38
|
+
}
|
|
39
|
+
const tmpOut = path.join(REPO_ROOT, 'node_modules', '.cache-prod-bundle.js');
|
|
40
|
+
execSync(
|
|
41
|
+
`bun build "${entry}" --outfile "${tmpOut}" --minify --target=browser --format=iife --define:process.env.NODE_ENV='"production"'`,
|
|
42
|
+
{ cwd: REPO_ROOT, stdio: 'inherit' }
|
|
43
|
+
);
|
|
44
|
+
return fs.readFileSync(tmpOut, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
ensureDevDeps();
|
|
49
|
+
const rfCss = fs.readFileSync(
|
|
50
|
+
path.join(REPO_ROOT, 'node_modules', '@xyflow', 'react', 'dist', 'style.css'),
|
|
51
|
+
'utf8'
|
|
52
|
+
);
|
|
53
|
+
const customCss = fs.readFileSync(path.join(__dirname, 'styles.css'), 'utf8');
|
|
54
|
+
const rawBundle = await compileBundle();
|
|
55
|
+
const safeBundle = rawBundle
|
|
56
|
+
.replace(/import\.meta\.env/g, '({MODE:"production"})')
|
|
57
|
+
.replace(/<\/script/gi, '<\\/script');
|
|
58
|
+
|
|
59
|
+
const htmlTemplate = `<!DOCTYPE html>
|
|
60
|
+
<html lang="__ATLAS_HTML_LANG__">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="UTF-8" />
|
|
63
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
64
|
+
<meta name="theme-color" content="#fafafa" />
|
|
65
|
+
<title>__ATLAS_HTML_TITLE__</title>
|
|
66
|
+
<style>${rfCss}\n${customCss}</style>
|
|
67
|
+
</head>
|
|
68
|
+
<body>
|
|
69
|
+
<div id="root"></div>
|
|
70
|
+
<script id="atlas-data-json" type="application/json">__ATLAS_DATA_JSON__</script>
|
|
71
|
+
<script>${safeBundle}</script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const outAsset = path.join(REPO_ROOT, 'assets', 'viewer-shell.html');
|
|
77
|
+
fs.mkdirSync(path.dirname(outAsset), { recursive: true });
|
|
78
|
+
fs.writeFileSync(outAsset, htmlTemplate, 'utf8');
|
|
79
|
+
console.log(`Built assets/viewer-shell.html (${(fs.statSync(outAsset).size / 1024).toFixed(1)} KB)`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main();
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const args = { port: 5179, data: path.join(__dirname, 'sample-data.json') };
|
|
13
|
+
for (let i = 2; i < argv.length; i++) {
|
|
14
|
+
if (argv[i] === '--port' && argv[i + 1]) args.port = Number(argv[++i]);
|
|
15
|
+
else if (argv[i] === '--data' && argv[i + 1]) args.data = path.resolve(argv[++i]);
|
|
16
|
+
}
|
|
17
|
+
return args;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureDevDeps() {
|
|
21
|
+
const rfCss = path.join(REPO_ROOT, 'node_modules', '@xyflow', 'react', 'dist', 'style.css');
|
|
22
|
+
if (fs.existsSync(rfCss)) return;
|
|
23
|
+
console.log('Installing template runtime dependencies in repository…');
|
|
24
|
+
try {
|
|
25
|
+
execSync('bun install', { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
26
|
+
} catch (_) {
|
|
27
|
+
execSync('npm install', { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function bundleAppJs() {
|
|
32
|
+
const entry = path.join(__dirname, 'app.jsx');
|
|
33
|
+
const esbuildPath = path.join(REPO_ROOT, 'node_modules', 'esbuild', 'lib', 'main.js');
|
|
34
|
+
if (fs.existsSync(esbuildPath)) {
|
|
35
|
+
const esbuild = await import(esbuildPath);
|
|
36
|
+
const res = await esbuild.build({
|
|
37
|
+
entryPoints: [entry],
|
|
38
|
+
bundle: true,
|
|
39
|
+
write: false,
|
|
40
|
+
format: 'iife',
|
|
41
|
+
target: ['es2020'],
|
|
42
|
+
jsx: 'automatic',
|
|
43
|
+
define: {
|
|
44
|
+
'process.env.NODE_ENV': '"development"',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return res.outputFiles[0].text.replace(/import\.meta\.env/g, '({MODE:"development"})');
|
|
48
|
+
}
|
|
49
|
+
const tmpOut = path.join(REPO_ROOT, 'node_modules', '.cache-dev-bundle.js');
|
|
50
|
+
execSync(
|
|
51
|
+
`bun build "${entry}" --outfile "${tmpOut}" --target=browser --format=iife --define:process.env.NODE_ENV='"development"'`,
|
|
52
|
+
{ cwd: REPO_ROOT, stdio: 'pipe' }
|
|
53
|
+
);
|
|
54
|
+
return fs.readFileSync(tmpOut, 'utf8').replace(/import\.meta\.env/g, '({MODE:"development"})');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function start() {
|
|
58
|
+
const args = parseArgs(process.argv);
|
|
59
|
+
ensureDevDeps();
|
|
60
|
+
|
|
61
|
+
const rfCssPath = path.join(REPO_ROOT, 'node_modules', '@xyflow', 'react', 'dist', 'style.css');
|
|
62
|
+
const stylesPath = path.join(__dirname, 'styles.css');
|
|
63
|
+
const appPath = path.join(__dirname, 'app.jsx');
|
|
64
|
+
|
|
65
|
+
let cachedBundle = await bundleAppJs();
|
|
66
|
+
const clients = new Set();
|
|
67
|
+
|
|
68
|
+
const broadcast = (type) => {
|
|
69
|
+
for (const res of clients) {
|
|
70
|
+
res.write(`data: ${JSON.stringify({ type, ts: Date.now() })}\n\n`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let debounceTimer = null;
|
|
75
|
+
const watchTargets = [stylesPath, appPath, args.data].filter((p) => fs.existsSync(p));
|
|
76
|
+
for (const file of watchTargets) {
|
|
77
|
+
fs.watch(file, () => {
|
|
78
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
79
|
+
debounceTimer = setTimeout(async () => {
|
|
80
|
+
try {
|
|
81
|
+
if (file === stylesPath) {
|
|
82
|
+
console.log('[dev] styles.css updated → hot-swapping CSS');
|
|
83
|
+
broadcast('css');
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`[dev] ${path.basename(file)} updated → rebuilding & reloading`);
|
|
86
|
+
cachedBundle = await bundleAppJs();
|
|
87
|
+
broadcast('reload');
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error('[dev] build error:', err.message);
|
|
91
|
+
}
|
|
92
|
+
}, 60);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const server = http.createServer((req, res) => {
|
|
97
|
+
const url = new URL(req.url || '/', `http://localhost:${args.port}`);
|
|
98
|
+
|
|
99
|
+
if (url.pathname === '/__events') {
|
|
100
|
+
res.writeHead(200, {
|
|
101
|
+
'Content-Type': 'text/event-stream',
|
|
102
|
+
'Cache-Control': 'no-cache',
|
|
103
|
+
Connection: 'keep-alive',
|
|
104
|
+
});
|
|
105
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
106
|
+
clients.add(res);
|
|
107
|
+
req.on('close', () => clients.delete(res));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (url.pathname === '/__styles.css') {
|
|
112
|
+
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
113
|
+
res.end(fs.readFileSync(stylesPath, 'utf8'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (url.pathname === '/__rf.css') {
|
|
118
|
+
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
119
|
+
res.end(fs.readFileSync(rfCssPath, 'utf8'));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (url.pathname === '/__bundle.js') {
|
|
124
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
125
|
+
res.end(cachedBundle);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rawData = fs.readFileSync(args.data, 'utf8');
|
|
130
|
+
const parsed = JSON.parse(rawData);
|
|
131
|
+
const safeJson = JSON.stringify(parsed).replace(/</g, '\\u003c');
|
|
132
|
+
|
|
133
|
+
const html = `<!DOCTYPE html>
|
|
134
|
+
<html lang="${parsed.meta?.language || 'zh-CN'}">
|
|
135
|
+
<head>
|
|
136
|
+
<meta charset="UTF-8" />
|
|
137
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
138
|
+
<meta name="theme-color" content="#fafafa" />
|
|
139
|
+
<title>${parsed.meta?.title || 'Atlas Loop'} — Dev Preview</title>
|
|
140
|
+
<link rel="stylesheet" href="/__rf.css" />
|
|
141
|
+
<link id="atlas-live-css" rel="stylesheet" href="/__styles.css" />
|
|
142
|
+
</head>
|
|
143
|
+
<body>
|
|
144
|
+
<div id="root"></div>
|
|
145
|
+
<script id="atlas-data-json" type="application/json">${safeJson}</script>
|
|
146
|
+
<script src="/__bundle.js"></script>
|
|
147
|
+
<script>
|
|
148
|
+
(() => {
|
|
149
|
+
const es = new EventSource('/__events');
|
|
150
|
+
es.onmessage = (ev) => {
|
|
151
|
+
const msg = JSON.parse(ev.data);
|
|
152
|
+
if (msg.type === 'css') {
|
|
153
|
+
const link = document.getElementById('atlas-live-css');
|
|
154
|
+
if (link) link.href = '/__styles.css?t=' + msg.ts;
|
|
155
|
+
} else if (msg.type === 'reload') {
|
|
156
|
+
window.location.reload();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
})();
|
|
160
|
+
</script>
|
|
161
|
+
</body>
|
|
162
|
+
</html>`;
|
|
163
|
+
|
|
164
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
165
|
+
res.end(html);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
server.listen(args.port, () => {
|
|
169
|
+
console.log(`\nAtlas Loop Template Dev Runtime running at: http://localhost:${args.port}`);
|
|
170
|
+
console.log(`- Edit template/styles.css → Instant CSS hot-swap (preserves canvas state)`);
|
|
171
|
+
console.log(`- Edit template/app.jsx → Auto-rebuild & reload`);
|
|
172
|
+
console.log(`- Data source → ${args.data}`);
|
|
173
|
+
console.log(`- Bake into viewer-shell → npm run build:shell\n`);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
start();
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "2.0",
|
|
3
|
+
"meta": {
|
|
4
|
+
"title": "意式浓缩萃取物理机制(Espresso Extraction)",
|
|
5
|
+
"subtitle": "从研磨粒径分布、渗流阻力到金杯风味平衡的因果体系",
|
|
6
|
+
"subjectTag": "食品物理 / 冲煮技艺",
|
|
7
|
+
"language": "zh-CN",
|
|
8
|
+
"generatedByModel": "Atlas Loop Preview",
|
|
9
|
+
"level": 0,
|
|
10
|
+
"drillDownTarget": null,
|
|
11
|
+
"parentMapHtml": null,
|
|
12
|
+
"editor": "vscode",
|
|
13
|
+
"uncoveredScopes": [
|
|
14
|
+
"不同烘焙度细胞壁孔隙率的热解化学动力学细节未在此全景图展开"
|
|
15
|
+
],
|
|
16
|
+
"unverifiedNotes": [
|
|
17
|
+
"极细粉在预浸泡阶段的电化学团聚效应在学界仍存在两种竞争假说"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"streams": [
|
|
21
|
+
{
|
|
22
|
+
"id": "stream-hydro",
|
|
23
|
+
"title": "流体渗透与阻力主线",
|
|
24
|
+
"summary": "高压热水穿过压实粉床时的渗流规律与通道风险",
|
|
25
|
+
"learningOrder": ["n-grind", "n-puck-prep", "n-channel-branch", "n-balance"],
|
|
26
|
+
"deepDive": {
|
|
27
|
+
"hoverHint": "深究变压曲线如何抑制后段粉饼结构崩解",
|
|
28
|
+
"askPrompts": [
|
|
29
|
+
{
|
|
30
|
+
"title": "推演恒压 9 bar 与渐降压曲线的后段流速差异",
|
|
31
|
+
"prompt": "在意式萃取中后段随着可溶物析出、粉饼机械强度下降,为什么将压力从 9 bar 平滑降至 6 bar 反而能维持更均匀的渗流并减少尾段木质涩感?请结合达西定律解释。"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"subAtlasPrompt": "使用 Atlas Loop 递归生成 L1 专题子地图,目标:「意式变压与变流量萃取曲线机制」,父级地图 espresso-extraction-atlas.html"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "stream-solubles",
|
|
39
|
+
"title": "风味物质分阶段溶出",
|
|
40
|
+
"summary": "酸、甜、苦不同分子量化合物随萃取推进的析出次序",
|
|
41
|
+
"learningOrder": ["n-grind", "n-solubility", "n-balance"],
|
|
42
|
+
"deepDive": {
|
|
43
|
+
"hoverHint": "深究水温梯度对极性与非极性风味分子的选择性",
|
|
44
|
+
"askPrompts": [
|
|
45
|
+
{
|
|
46
|
+
"title": "对比浅烘与深烘豆在 88°C vs 94°C 下的溶出速率曲线",
|
|
47
|
+
"prompt": "为什么浅烘高密度豆需要 93°C 以上水温来提升前中段甜感转化,而深烘豆在同样水温下极易在尾段析出过量绿原酸内酯导致焦苦?"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"subAtlasPrompt": "使用 Atlas Loop 递归生成 L1 专题子地图,目标:「不同烘焙度风味化合物的溶解动力学」,父级地图 espresso-extraction-atlas.html"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"nodes": [
|
|
55
|
+
{
|
|
56
|
+
"id": "n-grind",
|
|
57
|
+
"streamIds": ["stream-hydro", "stream-solubles"],
|
|
58
|
+
"role": "entry",
|
|
59
|
+
"title": "双峰粒径分布(主粒径与极细粉)",
|
|
60
|
+
"summary": "磨豆机刀盘决定粗细颗粒比例,同时设定总比表面积与粉床孔隙率",
|
|
61
|
+
"whyItMatters": "它是流速与风味溶出速率的共同源头,微调几微米即可改变整体平衡",
|
|
62
|
+
"keyFactors": {
|
|
63
|
+
"inputs": "咖啡豆脆度、刀盘几何(平刀 / 锥刀)、转速 RPM",
|
|
64
|
+
"outputs": "主峰粒径(200–400 μm)+ 极细粉占比(< 100 μm)"
|
|
65
|
+
},
|
|
66
|
+
"mechanismsAndEffects": "极细粉贡献绝大部分瞬时接触表面积,而主峰颗粒搭建支撑骨架防止粉床完全堵死",
|
|
67
|
+
"watchouts": "并非粒径越均匀越好:完全筛除极细粉会令意式浓缩失去醇厚度(Body)与油脂挂杯感",
|
|
68
|
+
"confidence": "verified",
|
|
69
|
+
"anchors": [
|
|
70
|
+
{
|
|
71
|
+
"kind": "snippet",
|
|
72
|
+
"label": "达西渗流简化关系",
|
|
73
|
+
"content": "Q = (k · A · ΔP) / (μ · L) // 渗透率 k 对粒径平方高度敏感"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"deepDive": {
|
|
77
|
+
"hoverHint": "深究平刀与锥刀在粒径双峰形态与风味清晰度上的差异",
|
|
78
|
+
"askPrompts": [
|
|
79
|
+
{
|
|
80
|
+
"title": "对比大直径平刀与锥刀的粒径分布曲线及风味表现",
|
|
81
|
+
"prompt": "从磨豆机粉碎路径(多次切削 vs 挤压破碎)分析:为什么高同心度平刀通常呈现更窄的主峰与更高风味分离度,而锥刀往往带来更宽的双峰与更圆润厚重的触感?"
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"subAtlasPrompt": "使用 Atlas Loop 递归生成 L1 专题子地图,目标:「平刀与锥刀粉碎几何学与粒径控制」,父级地图 espresso-extraction-atlas.html"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "n-puck-prep",
|
|
89
|
+
"streamIds": ["stream-hydro"],
|
|
90
|
+
"role": "core",
|
|
91
|
+
"title": "布粉均质化与低压预浸泡",
|
|
92
|
+
"summary": "打散静电结块并缓慢润湿粉饼纤维,消除局部低密度薄弱区",
|
|
93
|
+
"whyItMatters": "高压水流具有极强的马太效应,任何初始密度不均都会被瞬间放大为击穿通道",
|
|
94
|
+
"keyFactors": {
|
|
95
|
+
"inputs": "针式布粉(WDT)、垂直恒力压粉、2–3 bar 低压浸润",
|
|
96
|
+
"outputs": "各向同性膨胀的饱和粉饼"
|
|
97
|
+
},
|
|
98
|
+
"mechanismsAndEffects": "纤维吸水膨胀缩小大孔隙,使主升压阶段水流前锋保持水平推进",
|
|
99
|
+
"watchouts": "压粉力度超过 15 kg 后对粉床密度几乎无额外提升,压粉水平度远比绝对大力更关键",
|
|
100
|
+
"confidence": "verified",
|
|
101
|
+
"anchors": [],
|
|
102
|
+
"detailGraph": {
|
|
103
|
+
"nodes": [
|
|
104
|
+
{
|
|
105
|
+
"id": "sub-wdt",
|
|
106
|
+
"role": "core",
|
|
107
|
+
"title": "0.3mm 细针打散静电团块",
|
|
108
|
+
"summary": "破坏落粉时形成的局部高密核心与架桥空洞",
|
|
109
|
+
"anchors": []
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "sub-bloom",
|
|
113
|
+
"role": "foundation",
|
|
114
|
+
"title": "二氧化碳排空与纤维润胀",
|
|
115
|
+
"summary": "低压阶段排出烘焙残余气体,降低气阻干扰",
|
|
116
|
+
"anchors": []
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
"edges": [
|
|
120
|
+
{
|
|
121
|
+
"id": "se-prep",
|
|
122
|
+
"source": "sub-wdt",
|
|
123
|
+
"target": "sub-bloom",
|
|
124
|
+
"label": "恒力压平后通水润湿"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"deepDive": {
|
|
129
|
+
"hoverHint": "深究养豆期二氧化碳残留如何诱发微通道气阻",
|
|
130
|
+
"askPrompts": [
|
|
131
|
+
{
|
|
132
|
+
"title": "分析极新鲜烘焙豆(< 3 天)萃取流速失稳的物理原因",
|
|
133
|
+
"prompt": "刚出炉不足 3 天的咖啡豆在 9 bar 热水接触下释放大量 CO2 微气泡,这些气泡如何在粉饼孔隙中形成局部气锁(Gas Lock)并迫使水流改道产生通道效应?"
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"subAtlasPrompt": "使用 Atlas Loop 递归生成 L1 专题子地图,目标:「养豆排气的物理扩散模型与预浸泡参数匹配」,父级地图 espresso-extraction-atlas.html"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "n-channel-branch",
|
|
141
|
+
"streamIds": ["stream-hydro"],
|
|
142
|
+
"role": "branch",
|
|
143
|
+
"title": "临界压力下的均匀层流 vs 局部通道击穿",
|
|
144
|
+
"summary": "粉饼结构能否承受 6–9 bar 压差而不产生局部裂隙高速射流",
|
|
145
|
+
"whyItMatters": "决定一杯浓缩是甜润均衡,还是同时出现尖酸(周边欠萃)与干涩(通道壁严重过萃)",
|
|
146
|
+
"keyFactors": {
|
|
147
|
+
"inputs": "粉饼均匀度、泵压爬升斜率、顶空余量(Headspace)",
|
|
148
|
+
"outputs": "均匀层流析出 | 局部喷射与偏流"
|
|
149
|
+
},
|
|
150
|
+
"mechanismsAndEffects": "调得过细反而会因局部阻力过高被高压直接撕裂粉床,导致实测总萃取率不升反降",
|
|
151
|
+
"watchouts": "切勿只盯总时间:同样 28 秒出液 36g,可能掩盖了前慢后喷的隐性通道",
|
|
152
|
+
"confidence": "unverified",
|
|
153
|
+
"unverifiedReason": "极细粉在高压下究竟是填补微裂隙自愈还是加速堵塞,目前流体实验尚存争议",
|
|
154
|
+
"anchors": [
|
|
155
|
+
{
|
|
156
|
+
"kind": "source",
|
|
157
|
+
"label": "实验研究参考",
|
|
158
|
+
"citation": "Cameron et al. (2020), Systematically Improving Espresso, Matter 2(3), 631–648"
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"deepDive": {
|
|
162
|
+
"hoverHint": "深究 Cameron 论文发现的「研磨过细反而降低萃取率」反直觉拐点",
|
|
163
|
+
"askPrompts": [
|
|
164
|
+
{
|
|
165
|
+
"title": "精读 Matter 2020 论文中研磨度与萃取率的倒 U 型拐点",
|
|
166
|
+
"prompt": "为什么在 Cameron 等人 2020 年发表于《Matter》的意式浓缩模型与实验中,当磨豆机刻度越过某个临界细度后,平均萃取率(EY%)会出现断崖式下跌且杯间方差急剧放大?"
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"subAtlasPrompt": "使用 Atlas Loop 递归生成 L1 专题子地图,目标:「意式浓缩研磨细度—萃取率倒 U 型拐点模型」,父级地图 espresso-extraction-atlas.html"
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "n-solubility",
|
|
174
|
+
"streamIds": ["stream-solubles"],
|
|
175
|
+
"role": "foundation",
|
|
176
|
+
"title": "风味化合物极性与分子量溶解梯队",
|
|
177
|
+
"summary": "果酸与花香最先溶出,美拉德甜感居中,大分子苦涩木质调最后析出",
|
|
178
|
+
"whyItMatters": "解释了为何截断粉液比(如 Ristretto 1:1.5 vs Lungo 1:3)能直接重塑风味结构",
|
|
179
|
+
"keyFactors": {
|
|
180
|
+
"inputs": "水温、接触时间、通过粉饼的总水量",
|
|
181
|
+
"outputs": "前段明亮酸香 → 中段焦糖甜醇 → 尾段苦韵与收敛感"
|
|
182
|
+
},
|
|
183
|
+
"mechanismsAndEffects": "高极性小分子有机酸几乎瞬间溶解,而疏水性油脂乳化与高分子黑精需更多溶剂冲刷",
|
|
184
|
+
"watchouts": "酸味过重往往不是豆子本身太酸,而是萃取不足导致中段甜感物质尚未充分析出",
|
|
185
|
+
"confidence": "verified",
|
|
186
|
+
"anchors": [
|
|
187
|
+
{
|
|
188
|
+
"kind": "snippet",
|
|
189
|
+
"label": "金杯萃取核心恒等式",
|
|
190
|
+
"content": "EY(%) = (液重 Beverage Mass × 浓度 TDS%) / 干粉重 Dose Mass"
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
"deepDive": {
|
|
194
|
+
"hoverHint": "深究浓度(TDS)与萃取率(EY)如何共同映射感官风味象限",
|
|
195
|
+
"askPrompts": [
|
|
196
|
+
{
|
|
197
|
+
"title": "辨析「浓淡(TDS)」与「过萃/欠萃(EY)」的独立调节手法",
|
|
198
|
+
"prompt": "当一杯意式浓缩喝起来口感偏薄(TDS 偏低)但已经出现尾段干涩(EY 偏高)时,应该如何同时调整研磨度与粉液比(Brew Ratio)来提高醇厚度并消除干涩感?"
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"subAtlasPrompt": "使用 Atlas Loop 递归生成 L1 专题子地图,目标:「意式金杯控制图(TDS vs EY)与感官纠偏矩阵」,父级地图 espresso-extraction-atlas.html"
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "n-balance",
|
|
206
|
+
"streamIds": ["stream-hydro", "stream-solubles"],
|
|
207
|
+
"role": "outcome",
|
|
208
|
+
"title": "金杯感官平衡(18%–22% EY · 8%–12% TDS)",
|
|
209
|
+
"summary": "均匀渗流叠加恰当截断比,实现酸甜苦协调与绵密油脂触感",
|
|
210
|
+
"whyItMatters": "将经验主义调磨转化为可量化、可复现的因果调参闭环",
|
|
211
|
+
"keyFactors": {
|
|
212
|
+
"inputs": "稳定的无通道渗流 + 匹配烘焙度的粉液比(常见 1:1.8–1:2.5)",
|
|
213
|
+
"outputs": "风味干净、甜感饱满、余韵持久的出品"
|
|
214
|
+
},
|
|
215
|
+
"mechanismsAndEffects": "每次只改变单一变量(优先固定粉重与水温,先调研磨稳流速,再微调液重定酸甜比)",
|
|
216
|
+
"watchouts": "折射仪数值仅为辅助锚点,最终风味须结合豆种处理法与品饮偏好综合裁定",
|
|
217
|
+
"confidence": "verified",
|
|
218
|
+
"anchors": []
|
|
219
|
+
}
|
|
220
|
+
],
|
|
221
|
+
"edges": [
|
|
222
|
+
{
|
|
223
|
+
"id": "e1",
|
|
224
|
+
"source": "n-grind",
|
|
225
|
+
"target": "n-puck-prep",
|
|
226
|
+
"label": "设定初始粒径与孔隙骨架"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"id": "e2",
|
|
230
|
+
"source": "n-puck-prep",
|
|
231
|
+
"target": "n-channel-branch",
|
|
232
|
+
"label": "升压至 6–9 bar 考验粉床均质度"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"id": "e3",
|
|
236
|
+
"source": "n-grind",
|
|
237
|
+
"target": "n-solubility",
|
|
238
|
+
"label": "决定固液接触比表面积"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"id": "e4",
|
|
242
|
+
"source": "n-channel-branch",
|
|
243
|
+
"target": "n-balance",
|
|
244
|
+
"label": "均匀层流保障全区域同步萃取"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"id": "e5",
|
|
248
|
+
"source": "n-solubility",
|
|
249
|
+
"target": "n-balance",
|
|
250
|
+
"label": "按目标粉液比精准截断出液"
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
}
|