@hoplogic/hopjit 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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/act-body-interpreter.d.ts +24 -0
  4. package/dist/act-body-interpreter.js +159 -0
  5. package/dist/act-body-parser.d.ts +10 -0
  6. package/dist/act-body-parser.js +577 -0
  7. package/dist/act-builtins.d.ts +12 -0
  8. package/dist/act-builtins.js +104 -0
  9. package/dist/ast-helpers.d.ts +26 -0
  10. package/dist/ast-helpers.js +58 -0
  11. package/dist/ast-runtime.d.ts +49 -0
  12. package/dist/ast-runtime.js +224 -0
  13. package/dist/ast-types.d.ts +260 -0
  14. package/dist/ast-types.js +4 -0
  15. package/dist/cli-types.d.ts +190 -0
  16. package/dist/cli-types.js +4 -0
  17. package/dist/cli.d.ts +26 -0
  18. package/dist/cli.js +623 -0
  19. package/dist/dispatcher.d.ts +92 -0
  20. package/dist/dispatcher.js +692 -0
  21. package/dist/doc-ref.d.ts +41 -0
  22. package/dist/doc-ref.js +214 -0
  23. package/dist/engine-traverse.d.ts +37 -0
  24. package/dist/engine-traverse.js +385 -0
  25. package/dist/engine.d.ts +141 -0
  26. package/dist/engine.js +1792 -0
  27. package/dist/errors.d.ts +47 -0
  28. package/dist/errors.js +34 -0
  29. package/dist/hoplog.d.ts +120 -0
  30. package/dist/hoplog.js +501 -0
  31. package/dist/parser.d.ts +7 -0
  32. package/dist/parser.js +802 -0
  33. package/dist/persistence.d.ts +60 -0
  34. package/dist/persistence.js +208 -0
  35. package/dist/prompt.d.ts +130 -0
  36. package/dist/prompt.js +1014 -0
  37. package/dist/provider-types.d.ts +134 -0
  38. package/dist/provider-types.js +3 -0
  39. package/dist/runtime-types.d.ts +84 -0
  40. package/dist/runtime-types.js +4 -0
  41. package/dist/tools.d.ts +16 -0
  42. package/dist/tools.js +141 -0
  43. package/dist/validator.d.ts +23 -0
  44. package/dist/validator.js +959 -0
  45. package/driver/codex/AGENTS.md +54 -0
  46. package/driver/hopskill-build/SKILL.md +137 -0
  47. package/driver/hopskill-build/references/spec-skeleton.md +132 -0
  48. package/driver/hopskill-build/references/step-type-cheatsheet.md +56 -0
  49. package/driver/hopskill-build/references/three-focus-rules.md +148 -0
  50. package/driver/hopspec-skill.md +187 -0
  51. package/driver/references/cli-discovery.md +36 -0
  52. package/driver/references/discovery.md +45 -0
  53. package/driver/references/driver-subagent.md +50 -0
  54. package/driver/references/parallel-worker.md +50 -0
  55. package/driver/references/step-execution-rules.md +47 -0
  56. package/package.json +52 -0
@@ -0,0 +1,41 @@
1
+ import type { DocRef, DocRefFragment } from './ast-types.js';
2
+ import type { SandboxConfig } from './provider-types.js';
3
+ /**
4
+ * 从文本提取 doc-ref。排除 #^ 前缀(那是 [[doc#^anc-*]] 段落锚点反链,非章节引用)。
5
+ * 同一 (doc, section) 去重。// @a: anc-rule-doc-ref-extract
6
+ */
7
+ export declare function extractDocRefs(text: string): DocRef[];
8
+ /** 章节切片结果:命中标题、章节正文、匹配级别(精确/归一化/子串/未命中)及歧义标记。见 [[doc-ref#^anc-exec-doc-ref-resolve]] */
9
+ export interface SliceResult {
10
+ heading: string;
11
+ content: string;
12
+ matched: 'exact' | 'normalized' | 'fuzzy' | 'none';
13
+ ambiguous?: boolean;
14
+ }
15
+ /**
16
+ * 在 markdown 文本中定位 section 标题并切出其章节正文(含子标题,到下一个同级/更高级标题止)。
17
+ * 三级匹配:精确 → 归一化序号 → 子串包含。// @a: anc-exec-doc-ref-resolve
18
+ */
19
+ export declare function sliceSection(lines: string[], section: string): SliceResult;
20
+ /** doc-ref 解析期文件未找到/章节无匹配/沙箱拒读时抛出,携带原始 DocRef,供 engine failStep 消费。见 [[doc-ref#^anc-exec-doc-ref-resolve]] */
21
+ export declare class DocRefError extends Error {
22
+ readonly ref: DocRef;
23
+ constructor(message: string, ref: DocRef);
24
+ }
25
+ /**
26
+ * 静态存在性校验(P15 用):文件存在 + 章节可匹配。返回 null 表示通过,否则返回错误说明。
27
+ * 不抛异常(validator 收集错误而非中断)。// @a: anc-rule-p15
28
+ */
29
+ export declare function checkDocRefExists(workspaceDir: string, sandbox: SandboxConfig, ref: DocRef): string | null;
30
+ /**
31
+ * 运行期解析一组 doc-ref 为章节内容片段。同文件只读一次(按 path 缓存)。
32
+ * 大节超 DEFLATE_THRESHOLD 时写 workZone/docref/ 并返 file_path 指针;
33
+ * workZone 空串(独立模式)时不卸载、内联全文。
34
+ * 文件/章节找不到 → 抛 DocRefError(dispatcher 据此 failStep)。// @a: anc-exec-doc-ref-resolve
35
+ */
36
+ export declare function resolveDocRefs(refs: DocRef[], workspaceDir: string, sandbox: SandboxConfig, workZone: string): DocRefFragment[];
37
+ /**
38
+ * 把解析出的 fragment 渲染为 L2d doc_ref_context 文本块。
39
+ * 大节(有 file_path)只放 $file 指针提示;小节内联全文。// @a: anc-exec-doc-ref-injection
40
+ */
41
+ export declare function formatDocRefContext(fragments: DocRefFragment[]): string;
@@ -0,0 +1,214 @@
1
+ // @module: doc-ref ^anc-struct-doc-ref
2
+ // doc-ref [[文档#章节]] 确定性精确引用解析。
3
+ // 概念: ../../HopSpec V3核心规范.md ^anc-exec-doc-ref
4
+ // 设计: design/spec-parser.md ^anc-rule-doc-ref-extract(提取)
5
+ // design/step-dispatcher.md ^anc-exec-doc-ref-resolve(解析)
6
+ // design/prompt-assembler.md ^anc-exec-doc-ref-injection(注入/deflate)
7
+ import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { DEFLATE_THRESHOLD } from './ast-helpers.js';
10
+ import { readSandboxedFile, resolveWorkspacePath } from './tools.js';
11
+ // 捕获组1=文档路径,组2=章节名;(?:\|别名)? 吞掉 Obsidian 别名后缀。
12
+ // 强制要求 #章节(无 # 的纯 [[doc]] 不匹配)。
13
+ const RE_DOC_REF = /\[\[([^\]#|]+)#([^\]|]+?)(?:\|[^\]]+)?\]\]/g;
14
+ const RE_HEADING = /^(#{1,6})\s+(.*\S)/;
15
+ const RE_FENCE = /^```/;
16
+ /**
17
+ * 从文本提取 doc-ref。排除 #^ 前缀(那是 [[doc#^anc-*]] 段落锚点反链,非章节引用)。
18
+ * 同一 (doc, section) 去重。// @a: anc-rule-doc-ref-extract
19
+ */
20
+ export function extractDocRefs(text) {
21
+ if (!text)
22
+ return [];
23
+ const seen = new Set();
24
+ const refs = [];
25
+ RE_DOC_REF.lastIndex = 0;
26
+ let m;
27
+ while ((m = RE_DOC_REF.exec(text)) !== null) {
28
+ const doc = m[1].trim();
29
+ const section = m[2].trim();
30
+ if (section.startsWith('^'))
31
+ continue; // 段落锚点反链,非章节引用
32
+ if (!doc || !section)
33
+ continue;
34
+ const key = `${doc} ${section}`;
35
+ if (seen.has(key))
36
+ continue;
37
+ seen.add(key);
38
+ refs.push({ doc, section });
39
+ }
40
+ return refs;
41
+ }
42
+ // 剥离章节名/标题的序号前缀做归一化匹配。四类前缀:
43
+ // ① 阿拉伯序号 `4` / `4.7`(分隔符可选) ② 中文数字序号 `二、`(分隔符必需——
44
+ // 避免 "十全大补" 的 "十" 被误剥) ③ 全角括号序号 `(一)` ④ 半角括号序号 `(1)`。
45
+ // "第三方" 因 "第" 不在数字集、无分隔符,不会被剥。
46
+ const RE_NUM_PREFIX = /^(?:\d+(?:\.\d+)*[.、\s]*|[一二三四五六七八九十百千零]+[、.\s]+|([^)]*)\s*|\([^)]*\)\s*)/;
47
+ function normalizeSection(s) {
48
+ return s.replace(RE_NUM_PREFIX, '').trim();
49
+ }
50
+ function scanHeadings(lines) {
51
+ const headings = [];
52
+ let inFence = false;
53
+ for (let i = 0; i < lines.length; i++) {
54
+ if (RE_FENCE.test(lines[i].trim())) {
55
+ inFence = !inFence;
56
+ continue;
57
+ }
58
+ if (inFence)
59
+ continue;
60
+ const hm = lines[i].match(RE_HEADING);
61
+ if (hm)
62
+ headings.push({ line: i, depth: hm[1].length, text: hm[2].trim() });
63
+ }
64
+ return headings;
65
+ }
66
+ /**
67
+ * 在 markdown 文本中定位 section 标题并切出其章节正文(含子标题,到下一个同级/更高级标题止)。
68
+ * 三级匹配:精确 → 归一化序号 → 子串包含。// @a: anc-exec-doc-ref-resolve
69
+ */
70
+ export function sliceSection(lines, section) {
71
+ const headings = scanHeadings(lines);
72
+ const target = section.trim();
73
+ const normTarget = normalizeSection(target);
74
+ let hit;
75
+ let matched = 'none';
76
+ let ambiguous = false;
77
+ // a. 精确
78
+ hit = headings.find(h => h.text === target);
79
+ if (hit)
80
+ matched = 'exact';
81
+ // b. 归一化序号相等
82
+ if (!hit && normTarget) {
83
+ hit = headings.find(h => normalizeSection(h.text) === normTarget);
84
+ if (hit)
85
+ matched = 'normalized';
86
+ }
87
+ // c. 子串包含(兜底,多命中取首 + 标 ambiguous)
88
+ if (!hit) {
89
+ const cands = headings.filter(h => h.text.includes(target) || (normTarget && normalizeSection(h.text).includes(normTarget)));
90
+ if (cands.length > 0) {
91
+ hit = cands[0];
92
+ matched = 'fuzzy';
93
+ ambiguous = cands.length > 1;
94
+ }
95
+ }
96
+ if (!hit)
97
+ return { heading: '', content: '', matched: 'none' };
98
+ // 切区间 [命中行+1, 下一个 depth <= 命中 depth 的标题行)
99
+ let end = lines.length;
100
+ for (const h of headings) {
101
+ if (h.line > hit.line && h.depth <= hit.depth) {
102
+ end = h.line;
103
+ break;
104
+ }
105
+ }
106
+ const content = lines.slice(hit.line + 1, end).join('\n').trim();
107
+ return { heading: hit.text, content, matched, ambiguous };
108
+ }
109
+ /** doc-ref 解析期文件未找到/章节无匹配/沙箱拒读时抛出,携带原始 DocRef,供 engine failStep 消费。见 [[doc-ref#^anc-exec-doc-ref-resolve]] */
110
+ export class DocRefError extends Error {
111
+ ref;
112
+ constructor(message, ref) {
113
+ super(message);
114
+ this.ref = ref;
115
+ }
116
+ }
117
+ // 候选路径:原名 + 补 .md 兜底(Obsidian 习惯省后缀)
118
+ function candidatePaths(doc) {
119
+ return doc.endsWith('.md') ? [doc] : [doc, `${doc}.md`];
120
+ }
121
+ /** 定位 doc-ref 文件:遍历 candidatePaths(含 .md 兜底),命中 existsSync 即返回该 cand,全不命中返回 undefined。
122
+ * 不吞 resolveWorkspacePath 的抛错(path traversal 等)——由调用方按各自语义处理。checkDocRefExists / resolveDocRefs 共用。 */
123
+ function resolveDocFile(workspaceDir, doc) {
124
+ for (const cand of candidatePaths(doc)) {
125
+ if (existsSync(resolveWorkspacePath(workspaceDir, cand)))
126
+ return cand;
127
+ }
128
+ return undefined;
129
+ }
130
+ /**
131
+ * 静态存在性校验(P15 用):文件存在 + 章节可匹配。返回 null 表示通过,否则返回错误说明。
132
+ * 不抛异常(validator 收集错误而非中断)。// @a: anc-rule-p15
133
+ */
134
+ export function checkDocRefExists(workspaceDir, sandbox, ref) {
135
+ let resolvedFile;
136
+ try {
137
+ resolvedFile = resolveDocFile(workspaceDir, ref.doc);
138
+ }
139
+ catch (e) {
140
+ return e instanceof Error ? e.message : String(e);
141
+ }
142
+ if (!resolvedFile)
143
+ return `文件未找到: ${ref.doc}`;
144
+ let text;
145
+ try {
146
+ text = readSandboxedFile(workspaceDir, sandbox, resolvedFile);
147
+ }
148
+ catch (e) {
149
+ return e instanceof Error ? e.message : String(e);
150
+ }
151
+ const slice = sliceSection(text.split('\n'), ref.section);
152
+ if (slice.matched === 'none')
153
+ return `章节未匹配: [[${ref.doc}#${ref.section}]]`;
154
+ return null;
155
+ }
156
+ /**
157
+ * 运行期解析一组 doc-ref 为章节内容片段。同文件只读一次(按 path 缓存)。
158
+ * 大节超 DEFLATE_THRESHOLD 时写 workZone/docref/ 并返 file_path 指针;
159
+ * workZone 空串(独立模式)时不卸载、内联全文。
160
+ * 文件/章节找不到 → 抛 DocRefError(dispatcher 据此 failStep)。// @a: anc-exec-doc-ref-resolve
161
+ */
162
+ export function resolveDocRefs(refs, workspaceDir, sandbox, workZone) {
163
+ const fileCache = new Map();
164
+ const fragments = [];
165
+ for (const ref of refs) {
166
+ // 定位文件(含 .md 兜底)
167
+ const resolvedFile = resolveDocFile(workspaceDir, ref.doc);
168
+ if (!resolvedFile)
169
+ throw new DocRefError(`doc-ref 文件未找到: [[${ref.doc}#${ref.section}]]`, ref);
170
+ let lines = fileCache.get(resolvedFile);
171
+ if (!lines) {
172
+ lines = readSandboxedFile(workspaceDir, sandbox, resolvedFile).split('\n');
173
+ fileCache.set(resolvedFile, lines);
174
+ }
175
+ const slice = sliceSection(lines, ref.section);
176
+ if (slice.matched === 'none') {
177
+ throw new DocRefError(`doc-ref 章节未匹配: [[${ref.doc}#${ref.section}]]`, ref);
178
+ }
179
+ const frag = {
180
+ doc: ref.doc,
181
+ section: ref.section,
182
+ heading: slice.heading,
183
+ content: slice.content,
184
+ matched: slice.matched,
185
+ };
186
+ // deflate: 大节卸载到 work_zone 文件(独立模式 workZone 空串则内联)
187
+ if (workZone && Buffer.byteLength(slice.content, 'utf-8') > DEFLATE_THRESHOLD) {
188
+ const docrefDir = join(workZone, 'docref');
189
+ mkdirSync(docrefDir, { recursive: true, mode: 0o700 });
190
+ const safe = `${ref.doc}__${ref.section}`.replace(/[^\w一-龥.-]+/g, '_');
191
+ const filePath = join(docrefDir, `${safe}.md`);
192
+ writeFileSync(filePath, slice.content, { mode: 0o600 });
193
+ frag.file_path = filePath;
194
+ }
195
+ fragments.push(frag);
196
+ }
197
+ return fragments;
198
+ }
199
+ /**
200
+ * 把解析出的 fragment 渲染为 L2d doc_ref_context 文本块。
201
+ * 大节(有 file_path)只放 $file 指针提示;小节内联全文。// @a: anc-exec-doc-ref-injection
202
+ */
203
+ export function formatDocRefContext(fragments) {
204
+ if (fragments.length === 0)
205
+ return '';
206
+ const blocks = fragments.map(f => {
207
+ const head = `[[${f.doc}#${f.section}]](命中标题:${f.heading})`;
208
+ if (f.file_path) {
209
+ return `### ${head}\n全文见 $file: ${f.file_path}(请 Read 获取完整章节)`;
210
+ }
211
+ return `### ${head}\n${f.content}`;
212
+ });
213
+ return `[L2d. 引用文档(作者指定,必读)]\n${blocks.join('\n\n---\n\n')}`;
214
+ }
@@ -0,0 +1,37 @@
1
+ import type { SpecAST, StepNode, ExitStep } from './ast-types.js';
2
+ import type { StepStatus, VariableStore } from './ast-runtime.js';
3
+ export declare function subtreeContainsConfirm(node: StepNode): boolean;
4
+ /** DFS 遍历共享状态——步骤状态映射、变量存储、loop 计数器及 newlyRunning/newlyDone 回收通道。见 [[exec-engine#^anc-exec-dfs-traversal]] */
5
+ export interface TraversalState {
6
+ stepStates: Map<string, StepStatus>;
7
+ variables: VariableStore;
8
+ loopCounters: Map<string, number>;
9
+ newlyRunning?: StepNode[];
10
+ newlyDone?: {
11
+ node: StepNode;
12
+ failed: boolean;
13
+ }[];
14
+ }
15
+ /** DFS 遍历结果——executable(命中可执行步骤)/exit(触发提前退出)/retry(控制流后重新遍历)/none(无可执行节点)四态之一。见 [[exec-engine#^anc-exec-dfs-traversal]] */
16
+ export type TraversalResult = {
17
+ kind: 'executable';
18
+ step: StepNode;
19
+ } | {
20
+ kind: 'exit';
21
+ step: ExitStep;
22
+ } | {
23
+ kind: 'retry';
24
+ } | {
25
+ kind: 'none';
26
+ };
27
+ /** 深度优先前序遍历步骤树,定位下一个可执行节点:容器自动展开标 running、branch 内部评估条件、loop/控制流状态转移。见 [[exec-engine#^anc-exec-dfs-traversal]] */
28
+ export declare function dfsNextStep(steps: StepNode[], state: TraversalState, spec: SpecAST): TraversalResult;
29
+ export declare function collectParallelBatch(steps: StepNode[], state: TraversalState, spec: SpecAST, inParallelContext?: boolean): {
30
+ parallelStepId: string;
31
+ children: StepNode[];
32
+ } | null;
33
+ /** 完成级联:某步终态后递归向上检查父容器,全 children 终态则容器完成并按声明 +→ 提升聚合输出、loop 触发下轮迭代。见 [[exec-engine#^anc-exec-completion-cascade]] */
34
+ export declare function propagateCompletion(stepId: string, state: TraversalState, spec: SpecAST): void;
35
+ /** 求值 branch case 条件表达式:`{var}` 真值判定、`{var}==literal` / `{var}!=literal` 字符串比较,空条件恒真(default case)。见 [[exec-engine#^anc-exec-dfs-traversal]] */
36
+ export declare function evaluateCondition(condition: string | undefined, variables: VariableStore, scopeId: string): boolean;
37
+ export declare function collectDescendants(node: StepNode): StepNode[];
@@ -0,0 +1,385 @@
1
+ import { EXECUTABLE_STEP_TYPES, CONTAINER_STEP_TYPES, hasChildren, getChildren, getParentStepId } from './ast-helpers.js';
2
+ import { getWriteScope, buildStepMap, SCOPE_CREATING_TYPES, isTruthy, isTerminalStatus } from './ast-runtime.js';
3
+ const DEFAULT_MAX_ITERATIONS = 100;
4
+ // 静态判定 child 子树是否含 confirm——含则不可并行(多 worker 同时暂停 confirm 无法干净串行问人)。
5
+ // DFS 遍历所有后代;call 步骤的 callee 无法静态展开,保守不视为含 confirm(留运行时,同 P8 哲学)。
6
+ // 见 design/parallel-execution.md ^anc-exec-parallel-confirm-exclude。// @a: anc-exec-parallel-confirm-exclude
7
+ export function subtreeContainsConfirm(node) {
8
+ if (node.step_type === 'confirm')
9
+ return true;
10
+ if (hasChildren(node)) {
11
+ for (const child of getChildren(node)) {
12
+ if (subtreeContainsConfirm(child))
13
+ return true;
14
+ }
15
+ }
16
+ return false;
17
+ }
18
+ /** 深度优先前序遍历步骤树,定位下一个可执行节点:容器自动展开标 running、branch 内部评估条件、loop/控制流状态转移。见 [[exec-engine#^anc-exec-dfs-traversal]] */
19
+ export function dfsNextStep(steps, state, spec) {
20
+ for (const step of steps) {
21
+ const status = state.stepStates.get(step.step_id);
22
+ if (isTerminalStatus(status))
23
+ continue;
24
+ if (status === 'pending') {
25
+ if (step.step_type === 'break') {
26
+ handleBreak(step, state, spec);
27
+ return { kind: 'retry' };
28
+ }
29
+ if (step.step_type === 'continue') {
30
+ handleContinue(step, state, spec);
31
+ return { kind: 'retry' };
32
+ }
33
+ if (step.step_type === 'exit') {
34
+ handleExit(step, state, spec);
35
+ return { kind: 'exit', step: step };
36
+ }
37
+ if (EXECUTABLE_STEP_TYPES.has(step.step_type)) {
38
+ // 叶子 `+ → x = 初值` 每次执行前重置:写入 default 到叶子的 write scope(最近容器 scope)。
39
+ // loop 内每轮该步执行即每轮重置(Python `x=None` 语义)。仅带 default 的输出触发,opt-in。
40
+ // 见 ^anc-exec-output-init。// @a: anc-exec-output-init
41
+ if (step.outputs?.some(o => o.default !== undefined)) {
42
+ const leafScope = getWriteScope(step.step_id, spec);
43
+ for (const o of step.outputs) {
44
+ if (o.default !== undefined)
45
+ state.variables.write(o.name, o.default, leafScope);
46
+ }
47
+ }
48
+ return { kind: 'executable', step };
49
+ }
50
+ if (CONTAINER_STEP_TYPES.has(step.step_type)) {
51
+ state.stepStates.set(step.step_id, 'running');
52
+ state.newlyRunning?.push(step);
53
+ ensureScope(step, state, spec);
54
+ if (step.step_type === 'branch') {
55
+ const evalResult = handleBranchEntry(step, state, spec);
56
+ if (evalResult === 'error')
57
+ return { kind: 'none' };
58
+ }
59
+ if (step.step_type === 'loop') {
60
+ const loop = step;
61
+ const maxIter = loop.max_iterations ?? DEFAULT_MAX_ITERATIONS;
62
+ if (maxIter === 0) {
63
+ state.stepStates.set(step.step_id, 'done');
64
+ skipAllChildren(step, state);
65
+ continue;
66
+ }
67
+ state.loopCounters.set(step.step_id, 1);
68
+ }
69
+ // for-each parallel:不进入子树(模板 child 是展开模板,不直接执行),
70
+ // 交给 collectParallelBatch 动态展开。见 design/parallel-execution.md §9b
71
+ // @a: anc-exec-parallel-foreach
72
+ if (step.step_type === 'parallel' && step.forEach) {
73
+ continue;
74
+ }
75
+ const children = getChildren(step);
76
+ if (children.length > 0) {
77
+ const result = dfsNextStep(children, state, spec);
78
+ if (result.kind !== 'none')
79
+ return result;
80
+ }
81
+ continue;
82
+ }
83
+ }
84
+ if (status === 'running' && CONTAINER_STEP_TYPES.has(step.step_type)) {
85
+ const children = getChildren(step);
86
+ if (children.length > 0) {
87
+ const result = dfsNextStep(children, state, spec);
88
+ if (result.kind !== 'none')
89
+ return result;
90
+ }
91
+ continue;
92
+ }
93
+ }
94
+ return { kind: 'none' };
95
+ }
96
+ // 复用模式真并行:找最外层 running/pending parallel,返回其 pending 直接 children(容器),供批量 fan-out。
97
+ // "最外层"判定:DFS 下行携带 inParallelContext——遇 parallel 且 !inParallelContext 即最外层;进入其子树后
98
+ // 置 true,内层再遇 parallel 不扇出(退化顺序模拟)。S12 保证 children 无依赖→串/并等价。
99
+ // @a: anc-exec-parallel-batch, anc-exec-parallel-one-layer
100
+ export function collectParallelBatch(steps, state, spec, inParallelContext = false) {
101
+ for (const step of steps) {
102
+ const status = state.stepStates.get(step.step_id);
103
+ if (isTerminalStatus(status))
104
+ continue;
105
+ // 最外层 parallel(pending 或 running,但前序 sibling 必须全部终态——否则前序步骤还没完成不能 fan-out)
106
+ if (step.step_type === 'parallel' && !inParallelContext) {
107
+ // 检查本层级中 parallel 之前的 sibling 是否都已终态
108
+ const siblings = steps;
109
+ const myIndex = siblings.indexOf(step);
110
+ const allPriorDone = siblings.slice(0, myIndex).every(s => isTerminalStatus(state.stepStates.get(s.step_id)));
111
+ if (!allPriorDone)
112
+ continue;
113
+ const children = getChildren(step);
114
+ const pendingChildren = children.filter(c => {
115
+ const s = state.stepStates.get(c.step_id);
116
+ return s === undefined || s === 'pending';
117
+ });
118
+ // 含 confirm 子树的 child 结构性排除出并行批次,保持 pending 留主循环串行推进。
119
+ // 见 design/parallel-execution.md ^anc-exec-parallel-confirm-exclude。// @a: anc-exec-parallel-confirm-exclude
120
+ const parallelizable = pendingChildren.filter(c => !subtreeContainsConfirm(c));
121
+ // 无可并行 child(全空 / 剩的全含 confirm)→ 此 parallel 无并行批次,交主循环串行 dfsNextStep
122
+ if (parallelizable.length === 0)
123
+ continue;
124
+ return { parallelStepId: step.step_id, children: parallelizable };
125
+ }
126
+ // 进入容器子树继续找(running 的容器才下钻;pending 的非 parallel 容器尚未进入,跳过交 dfsNextStep 处理)
127
+ if (CONTAINER_STEP_TYPES.has(step.step_type) && status === 'running') {
128
+ const nextInParallel = inParallelContext || step.step_type === 'parallel';
129
+ const found = collectParallelBatch(getChildren(step), state, spec, nextInParallel);
130
+ if (found)
131
+ return found;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ /** 完成级联:某步终态后递归向上检查父容器,全 children 终态则容器完成并按声明 +→ 提升聚合输出、loop 触发下轮迭代。见 [[exec-engine#^anc-exec-completion-cascade]] */
137
+ export function propagateCompletion(stepId, state, spec) {
138
+ const parentId = getParentStepId(stepId);
139
+ if (!parentId)
140
+ return;
141
+ const stepMap = buildStepMap(spec);
142
+ const parent = stepMap.get(parentId);
143
+ if (!parent)
144
+ return;
145
+ if (!CONTAINER_STEP_TYPES.has(parent.step_type))
146
+ return;
147
+ const children = getChildren(parent);
148
+ const allTerminal = children.every(c => isTerminalStatus(state.stepStates.get(c.step_id)));
149
+ if (!allTerminal)
150
+ return;
151
+ if (parent.step_type === 'loop') {
152
+ const loop = parent;
153
+ const maxIter = loop.max_iterations ?? DEFAULT_MAX_ITERATIONS;
154
+ const currentIter = state.loopCounters.get(parent.step_id) ?? 1;
155
+ if (currentIter < maxIter) {
156
+ const allChildrenDoneOrSkipped = children.every(c => {
157
+ const s = state.stepStates.get(c.step_id);
158
+ return s === 'done' || s === 'skipped';
159
+ });
160
+ if (allChildrenDoneOrSkipped) {
161
+ state.loopCounters.set(parent.step_id, currentIter + 1);
162
+ resetChildrenToPending(children, state);
163
+ // Python 语义:变量跨迭代自然保留,引擎不清当轮变量。
164
+ // 需每轮重置的量由作者在子步骤显式 `+ → x = Null`(叶子每轮执行即重置)。
165
+ // 见 [[exec-engine]] 变量作用域规则 6 / 概念 ^anc-exec-loop-var-scope。// @a: anc-exec-loop-var-scope
166
+ return;
167
+ }
168
+ }
169
+ }
170
+ // Promote container declared outputs to parent scope
171
+ if (parent.outputs && parent.outputs.length > 0 && SCOPE_CREATING_TYPES.has(parent.step_type)) {
172
+ const containerScope = parentId;
173
+ const parentScope = getWriteScope(parentId, spec);
174
+ for (const decl of parent.outputs) {
175
+ const val = state.variables.read(decl.name, containerScope);
176
+ if (val !== undefined) {
177
+ state.variables.write(decl.name, val, parentScope);
178
+ }
179
+ }
180
+ }
181
+ // branch 完成判定:被选中的 case 失败即 branch 失败(concept: 被激活的 case 失败 → branch 失败)
182
+ // 其余 case 为 skipped,仅当选中的 case(非 skipped)为 failed 时 branch failed
183
+ if (parent.step_type === 'branch') {
184
+ const activeFailed = children.some(c => state.stepStates.get(c.step_id) === 'failed');
185
+ state.stepStates.set(parentId, activeFailed ? 'failed' : 'done');
186
+ state.newlyDone?.push({ node: parent, failed: activeFailed });
187
+ propagateCompletion(parentId, state, spec);
188
+ return;
189
+ }
190
+ state.stepStates.set(parentId, 'done');
191
+ // parallel 容器 done 由 joinParallel 直接记录,此通道排除 parallel(避免重复)。
192
+ // 见 design/spec-observability.md ^anc-obs-step-done-timing。
193
+ if (parent.step_type !== 'parallel') {
194
+ state.newlyDone?.push({ node: parent, failed: false });
195
+ }
196
+ propagateCompletion(parentId, state, spec);
197
+ }
198
+ /** 求值 branch case 条件表达式:`{var}` 真值判定、`{var}==literal` / `{var}!=literal` 字符串比较,空条件恒真(default case)。见 [[exec-engine#^anc-exec-dfs-traversal]] */
199
+ export function evaluateCondition(condition, variables, scopeId) {
200
+ if (!condition || condition.trim() === '')
201
+ return true;
202
+ const trimmed = condition.trim();
203
+ const neqIdx = trimmed.indexOf('!=');
204
+ if (neqIdx >= 0) {
205
+ const varName = extractVarName(trimmed.slice(0, neqIdx).trim());
206
+ const literal = stripQuotes(trimmed.slice(neqIdx + 2).trim());
207
+ const value = variables.read(varName, scopeId);
208
+ return String(value) !== literal;
209
+ }
210
+ const eqIdx = trimmed.indexOf('==');
211
+ if (eqIdx >= 0) {
212
+ const varName = extractVarName(trimmed.slice(0, eqIdx).trim());
213
+ const literal = stripQuotes(trimmed.slice(eqIdx + 2).trim());
214
+ const value = variables.read(varName, scopeId);
215
+ return String(value) === literal;
216
+ }
217
+ const varName = extractVarName(trimmed);
218
+ const value = variables.read(varName, scopeId);
219
+ return isTruthy(value);
220
+ }
221
+ function extractVarName(expr) {
222
+ if (expr.startsWith('{') && expr.endsWith('}')) {
223
+ return expr.slice(1, -1).trim();
224
+ }
225
+ return expr.trim();
226
+ }
227
+ function stripQuotes(s) {
228
+ if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"'))) {
229
+ return s.slice(1, -1);
230
+ }
231
+ return s;
232
+ }
233
+ function handleBranchEntry(branch, state, spec) {
234
+ const scopeId = getWriteScope(branch.step_id, spec);
235
+ const cases = branch.children;
236
+ for (let i = 0; i < cases.length; i++) {
237
+ const caseStep = cases[i];
238
+ const isDefault = !caseStep.condition || caseStep.condition.trim() === '' || caseStep.condition.trim() === 'default';
239
+ const matches = isDefault || evaluateCondition(caseStep.condition, state.variables, scopeId);
240
+ if (matches) {
241
+ state.stepStates.set(caseStep.step_id, 'running');
242
+ ensureScope(caseStep, state, spec);
243
+ // 选中的 case 进 newlyRunning(branch 自身已被 dfsNextStep 加入),
244
+ // nextStep 据此为 case 容器记录 step_start 骨架。未选中走 skipCaseRecursive。
245
+ // 见 design/spec-observability.md ^anc-obs-step-start-timing。// @a: anc-obs-step-start-timing
246
+ state.newlyRunning?.push(caseStep);
247
+ for (let j = 0; j < cases.length; j++) {
248
+ if (j !== i) {
249
+ skipCaseRecursive(cases[j], state);
250
+ }
251
+ }
252
+ return 'ok';
253
+ }
254
+ }
255
+ // No case matched — silent skip: mark branch done, all cases skipped, outputs None
256
+ for (const caseStep of cases) {
257
+ skipCaseRecursive(caseStep, state);
258
+ }
259
+ if (branch.outputs) {
260
+ const branchScope = getWriteScope(branch.step_id, spec);
261
+ for (const out of branch.outputs) {
262
+ state.variables.write(out.name, null, branchScope);
263
+ }
264
+ }
265
+ state.stepStates.set(branch.step_id, 'done');
266
+ return 'ok';
267
+ }
268
+ /** 从 stepId 向上找最近的 loop 祖先节点(break/continue 共用)。无则 null。 */
269
+ function findNearestLoopAncestor(stepId, spec) {
270
+ const stepMap = buildStepMap(spec);
271
+ let ancestorId = getParentStepId(stepId);
272
+ while (ancestorId) {
273
+ const ancestor = stepMap.get(ancestorId);
274
+ if (ancestor && ancestor.step_type === 'loop')
275
+ return ancestor;
276
+ ancestorId = getParentStepId(ancestorId);
277
+ }
278
+ return null;
279
+ }
280
+ function handleBreak(step, state, spec) {
281
+ state.stepStates.set(step.step_id, 'done');
282
+ const loop = findNearestLoopAncestor(step.step_id, spec);
283
+ if (loop) {
284
+ state.stepStates.set(loop.step_id, 'done');
285
+ skipRemainingInContainer(loop, state);
286
+ propagateCompletion(loop.step_id, state, spec);
287
+ }
288
+ }
289
+ function handleContinue(step, state, spec) {
290
+ state.stepStates.set(step.step_id, 'done');
291
+ const loop = findNearestLoopAncestor(step.step_id, spec);
292
+ if (loop) {
293
+ skipRemainingInContainer(loop, state);
294
+ propagateCompletion(step.step_id, state, spec);
295
+ }
296
+ }
297
+ function handleExit(step, state, spec) {
298
+ state.stepStates.set(step.step_id, 'done');
299
+ if (step.exit_outputs) {
300
+ const scopeId = getWriteScope(step.step_id, spec);
301
+ for (const [k, v] of Object.entries(step.exit_outputs)) {
302
+ state.variables.write(k, v, scopeId);
303
+ }
304
+ }
305
+ for (const [id, status] of state.stepStates) {
306
+ if (status === 'pending' || status === 'running') {
307
+ if (id !== step.step_id) {
308
+ state.stepStates.set(id, 'skipped');
309
+ }
310
+ }
311
+ }
312
+ }
313
+ function ensureScope(step, state, spec) {
314
+ if (!SCOPE_CREATING_TYPES.has(step.step_type))
315
+ return;
316
+ // 显式判"已存在"(loop 重进 / resume 已恢复),不建、不重灌初值——避免用 createScope 抛错做控制流。
317
+ if (state.variables.hasScope(step.step_id))
318
+ return;
319
+ const parentScope = getWriteScope(step.step_id, spec);
320
+ // parent scope 未建则跳过——worker 子实例 executeSubtreeOnly 收窄执行到 child 子树、父容器 scope 不建,
321
+ // 此时 child 容器的 parent 缺失是合法的(旧实现靠 createScope 抛错被 catch 吞,现显式判)。
322
+ if (!state.variables.hasScope(parentScope))
323
+ return;
324
+ state.variables.createScope(step.step_id, parentScope);
325
+ // 容器节点 `+ → acc = 初值` 的 init-once:仅首次创建 scope 时写入(loop 每轮重进/resume 恢复都提前 return,
326
+ // 不重灌 → 累加器保值)。见 ^anc-exec-output-init。// @a: anc-exec-output-init
327
+ if (step.outputs) {
328
+ for (const o of step.outputs) {
329
+ if (o.default !== undefined)
330
+ state.variables.write(o.name, o.default, step.step_id);
331
+ }
332
+ }
333
+ }
334
+ function skipCaseRecursive(caseStep, state) {
335
+ state.stepStates.set(caseStep.step_id, 'skipped');
336
+ skipAllChildren(caseStep, state);
337
+ }
338
+ // 标记子孙:仅 pending → skipped(未执行的才算跳过);已 done/failed/running/skipped 不动,
339
+ // 保留其真实执行状态(break 路径上已执行的子步骤不该被误标 skipped)
340
+ function skipAllChildren(step, state) {
341
+ if (!hasChildren(step))
342
+ return;
343
+ for (const child of getChildren(step)) {
344
+ if (state.stepStates.get(child.step_id) === 'pending') {
345
+ state.stepStates.set(child.step_id, 'skipped');
346
+ }
347
+ skipAllChildren(child, state);
348
+ }
349
+ }
350
+ // break/continue 收尾 loop 时调用:未执行的 pending → skipped;
351
+ // 执行路径上的 running(如命中的 branch/case)→ done(它执行到了 break/continue,不是被跳过);
352
+ // 已 done/failed/skipped 的不动。避免把已执行步骤误标 skipped。
353
+ function skipRemainingInContainer(container, state) {
354
+ if (!hasChildren(container))
355
+ return;
356
+ for (const child of getChildren(container)) {
357
+ const status = state.stepStates.get(child.step_id);
358
+ if (status === 'pending') {
359
+ state.stepStates.set(child.step_id, 'skipped');
360
+ skipAllChildren(child, state);
361
+ }
362
+ else if (status === 'running') {
363
+ state.stepStates.set(child.step_id, 'done');
364
+ skipAllChildren(child, state);
365
+ }
366
+ }
367
+ }
368
+ function resetChildrenToPending(children, state) {
369
+ for (const child of children) {
370
+ state.stepStates.set(child.step_id, 'pending');
371
+ if (hasChildren(child)) {
372
+ resetChildrenToPending(getChildren(child), state);
373
+ }
374
+ }
375
+ }
376
+ // 收集一个节点的所有后代(不含自身),用于 parallel child 子树输入解析、subtree 收窄等
377
+ export function collectDescendants(node) {
378
+ if (!hasChildren(node))
379
+ return [];
380
+ const out = [];
381
+ for (const c of getChildren(node)) {
382
+ out.push(c, ...collectDescendants(c));
383
+ }
384
+ return out;
385
+ }