@hoplogic/spec 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1120 @@
1
+ 'use strict';
2
+
3
+ // src/grammar.ts
4
+ var ZH_GRAMMAR = {
5
+ sectionNames: {
6
+ overview: "\u4EFB\u52A1\u6982\u8FF0",
7
+ input_def: "\u8F93\u5165\u5B9A\u4E49",
8
+ constraints: "\u786C\u6027\u7EA6\u675F",
9
+ execution: "\u6267\u884C\u6D41\u7A0B",
10
+ output_format: "\u8F93\u51FA\u683C\u5F0F",
11
+ input_example: "\u8F93\u5165\u65E5\u5FD7\u793A\u4F8B"
12
+ },
13
+ attrMap: {
14
+ "\u7C7B\u578B": "step_type",
15
+ "\u4EFB\u52A1": "task",
16
+ "\u8F93\u5165": "inputs",
17
+ "\u8F93\u51FA": "outputs",
18
+ "\u8F93\u51FA\u683C\u5F0F": "return_format",
19
+ "\u6838\u9A8C": "verifier",
20
+ "\u8BF4\u660E": "description",
21
+ "\u903B\u8F91": "description",
22
+ "\u6761\u4EF6": "condition",
23
+ "\u904D\u5386\u96C6\u5408": "collection",
24
+ "\u5143\u7D20\u53D8\u91CF": "element_var",
25
+ "\u6700\u5927\u8F6E\u6B21": "max_iterations",
26
+ "\u52A8\u4F5C": "action",
27
+ "\u76EE\u6807\u5FAA\u73AF": "target_loop",
28
+ "\u9000\u51FA\u6807\u8BC6": "exit_id",
29
+ "\u8C03\u7528\u76EE\u6807": "call_target",
30
+ "\u5DE5\u5177\u57DF": "tool_domain",
31
+ "Hoplet\u8DEF\u5F84": "hoplet_path",
32
+ "MCP\u670D\u52A1": "mcp_service",
33
+ "RAG\u77E5\u8BC6\u5E93": "rag_collection",
34
+ "\u5C55\u5F00\u6A21\u5F0F": "expand_mode",
35
+ "\u6700\u5927\u6B65\u6570": "max_steps",
36
+ "\u6700\u5927\u8F6E\u6570": "max_rounds",
37
+ "\u5141\u8BB8\u7C7B\u578B": "allowed_types",
38
+ "\u56FA\u5316\u8DEF\u5F84": "solidified_spec",
39
+ "\u6700\u5927\u6DF1\u5EA6": "max_depth"
40
+ },
41
+ stepPrefix: "\u6B65\u9AA4",
42
+ loopLabel: "loop",
43
+ branchLabel: "branch"
44
+ };
45
+ var EN_GRAMMAR = {
46
+ sectionNames: {
47
+ overview: "Overview",
48
+ input_def: "Input Definition",
49
+ constraints: "Constraints",
50
+ execution: "Execution Flow",
51
+ output_format: "Output Format",
52
+ input_example: "Input Example"
53
+ },
54
+ attrMap: {
55
+ "Type": "step_type",
56
+ "Task": "task",
57
+ "Input": "inputs",
58
+ "Output": "outputs",
59
+ "Output Format": "return_format",
60
+ "Verify": "verifier",
61
+ "Description": "description",
62
+ "Logic": "description",
63
+ "Condition": "condition",
64
+ "Collection": "collection",
65
+ "Element Variable": "element_var",
66
+ "Max Iterations": "max_iterations",
67
+ "Action": "action",
68
+ "Target Loop": "target_loop",
69
+ "Exit ID": "exit_id",
70
+ "Call Target": "call_target",
71
+ "Tool Domain": "tool_domain",
72
+ "Hoplet Path": "hoplet_path",
73
+ "MCP Service": "mcp_service",
74
+ "RAG Collection": "rag_collection",
75
+ "Expand Mode": "expand_mode",
76
+ "Max Steps": "max_steps",
77
+ "Max Rounds": "max_rounds",
78
+ "Allowed Types": "allowed_types",
79
+ "Solidified Spec": "solidified_spec",
80
+ "Max Depth": "max_depth"
81
+ },
82
+ stepPrefix: "Step",
83
+ loopLabel: "loop",
84
+ branchLabel: "branch"
85
+ };
86
+ function detectGrammar(markdown) {
87
+ if (markdown.includes("## \u6267\u884C\u6D41\u7A0B") || markdown.includes("#### \u6B65\u9AA4")) {
88
+ return ZH_GRAMMAR;
89
+ }
90
+ if (markdown.includes("## Execution Flow") || markdown.includes("#### Step")) {
91
+ return EN_GRAMMAR;
92
+ }
93
+ return ZH_GRAMMAR;
94
+ }
95
+
96
+ // src/spec-parser.ts
97
+ var ATTR_RE = /^(?<indent>\s*)-\s+(?<key>[^::]+)[::]\s*(?<value>.+)$/;
98
+ var SECTION_RE = /^##\s+(.+)$/;
99
+ function makeStepHeaderRe(grammar) {
100
+ const prefix = escapeRegex(grammar.stepPrefix);
101
+ return new RegExp(
102
+ `^(?<indent>\\s*)####\\s+${prefix}\\s*(?<id>[\\d.]+)(?:[\uFF1A:]\\s*(?<name>[^\uFF08(\\s]+))?`
103
+ );
104
+ }
105
+ function escapeRegex(s) {
106
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
107
+ }
108
+ function extractSections(markdown) {
109
+ const sections = {};
110
+ const lines = markdown.split("\n");
111
+ let currentTitle = "";
112
+ let currentLines = [];
113
+ for (const line of lines) {
114
+ const m = SECTION_RE.exec(line);
115
+ if (m) {
116
+ if (currentTitle) {
117
+ sections[currentTitle] = currentLines.join("\n").trim();
118
+ }
119
+ currentTitle = m[1].trim();
120
+ currentLines = [];
121
+ } else {
122
+ currentLines.push(line);
123
+ }
124
+ }
125
+ if (currentTitle) {
126
+ sections[currentTitle] = currentLines.join("\n").trim();
127
+ }
128
+ return sections;
129
+ }
130
+ function extractExecutionSection(markdown, grammar) {
131
+ const executionTitle = grammar.sectionNames["execution"] ?? "\u6267\u884C\u6D41\u7A0B";
132
+ const lines = markdown.split("\n");
133
+ const result = [];
134
+ let inSection = false;
135
+ for (const line of lines) {
136
+ const m = SECTION_RE.exec(line);
137
+ if (m) {
138
+ const title = m[1].trim();
139
+ if (title === executionTitle) {
140
+ inSection = true;
141
+ continue;
142
+ } else if (inSection) {
143
+ break;
144
+ }
145
+ }
146
+ if (inSection) {
147
+ result.push(line);
148
+ }
149
+ }
150
+ return result;
151
+ }
152
+ function splitIntoStepBlocks(lines, stepHeaderRe) {
153
+ const blocks = [];
154
+ let currentBlock = null;
155
+ for (const line of lines) {
156
+ const m = stepHeaderRe.exec(line);
157
+ if (m) {
158
+ if (currentBlock) blocks.push(currentBlock);
159
+ const indentStr = m.groups?.["indent"] ?? "";
160
+ currentBlock = {
161
+ indent: indentStr.length,
162
+ header: line,
163
+ body: []
164
+ };
165
+ } else if (currentBlock) {
166
+ currentBlock.body.push(line);
167
+ }
168
+ }
169
+ if (currentBlock) blocks.push(currentBlock);
170
+ return blocks;
171
+ }
172
+ function parseStepAttrs(bodyLines, attrMap) {
173
+ const attrs = {};
174
+ for (const line of bodyLines) {
175
+ const m = ATTR_RE.exec(line);
176
+ if (!m) continue;
177
+ const key = m.groups?.["key"]?.trim() ?? "";
178
+ const value = m.groups?.["value"]?.trim() ?? "";
179
+ const mappedKey = attrMap[key];
180
+ if (mappedKey) {
181
+ attrs[mappedKey] = value;
182
+ }
183
+ }
184
+ return attrs;
185
+ }
186
+ var EMPTY_MARKERS = /* @__PURE__ */ new Set(["(\u65E0)", "(none)", "\u65E0", "none", "\uFF08\u65E0\uFF09"]);
187
+ function parseInputs(raw) {
188
+ const trimmed = raw.trim();
189
+ if (!trimmed || EMPTY_MARKERS.has(trimmed)) return [];
190
+ return trimmed.split(/[,,]/).map((s) => s.trim()).filter(Boolean);
191
+ }
192
+ function parseOutputs(raw) {
193
+ const trimmed = raw.trim();
194
+ if (!trimmed) return [];
195
+ return trimmed.split(/[,,]/).map((s) => s.trim()).filter(Boolean);
196
+ }
197
+ function mapVerifyStrategy(raw) {
198
+ const trimmed = raw.trim();
199
+ if (trimmed === "\u9006\u5411" || trimmed.toLowerCase() === "reverse") return "reverse";
200
+ if (trimmed === "\u6B63\u5411\u4EA4\u53C9" || trimmed.toLowerCase() === "forward_cross") return "forward_cross";
201
+ return "none";
202
+ }
203
+ function createStepInfo(partial) {
204
+ return {
205
+ step_id: partial.step_id,
206
+ step_name: partial.step_name ?? "",
207
+ step_type: partial.step_type ?? "code",
208
+ description: partial.description ?? "",
209
+ task: partial.task ?? "",
210
+ inputs: partial.inputs ?? {},
211
+ outputs: partial.outputs ?? {},
212
+ return_format: partial.return_format ?? "",
213
+ verify_strategy: partial.verify_strategy ?? "none",
214
+ children: partial.children ?? [],
215
+ loop_mode: partial.loop_mode,
216
+ collection: partial.collection,
217
+ element_var: partial.element_var,
218
+ condition: partial.condition,
219
+ max_iterations: partial.max_iterations,
220
+ branch_condition: partial.branch_condition,
221
+ call_target: partial.call_target,
222
+ call_path: partial.call_path,
223
+ flow_action: partial.flow_action,
224
+ exit_id: partial.exit_id,
225
+ expand_mode: partial.expand_mode,
226
+ max_steps: partial.max_steps
227
+ };
228
+ }
229
+ function buildStepFromAttrs(stepId, stepName, attrs) {
230
+ const stepType = attrs["step_type"] ?? "code";
231
+ const step = createStepInfo({
232
+ step_id: stepId,
233
+ step_name: stepName || `step${stepId.replace(/\./g, "_")}`,
234
+ step_type: stepType
235
+ });
236
+ if (attrs["task"]) step.task = attrs["task"];
237
+ if (attrs["description"]) step.description = attrs["description"];
238
+ if (attrs["return_format"]) step.return_format = attrs["return_format"];
239
+ if (attrs["inputs"]) {
240
+ const names = parseInputs(attrs["inputs"]);
241
+ step.inputs = Object.fromEntries(names.map((n) => [n, n]));
242
+ }
243
+ if (attrs["outputs"]) {
244
+ const names = parseOutputs(attrs["outputs"]);
245
+ step.outputs = Object.fromEntries(names.map((n) => [n, n]));
246
+ }
247
+ if (attrs["verifier"]) {
248
+ step.verify_strategy = mapVerifyStrategy(attrs["verifier"]);
249
+ }
250
+ if (stepType === "loop") {
251
+ if (attrs["collection"]) {
252
+ step.loop_mode = "for-each";
253
+ step.collection = attrs["collection"];
254
+ step.element_var = attrs["element_var"] ?? "";
255
+ } else if (attrs["condition"]) {
256
+ step.loop_mode = "while";
257
+ step.condition = attrs["condition"];
258
+ }
259
+ if (attrs["max_iterations"]) {
260
+ const n = parseInt(attrs["max_iterations"], 10);
261
+ if (!isNaN(n)) step.max_iterations = n;
262
+ }
263
+ }
264
+ if (stepType === "branch" && attrs["condition"]) {
265
+ step.branch_condition = attrs["condition"];
266
+ }
267
+ if (stepType === "flow") {
268
+ step.flow_action = attrs["action"] ?? "exit";
269
+ if (attrs["exit_id"]) step.exit_id = attrs["exit_id"];
270
+ }
271
+ if (stepType === "call") {
272
+ step.call_target = attrs["call_target"] ?? "";
273
+ step.call_path = attrs["hoplet_path"] ?? attrs["tool_domain"] ?? attrs["mcp_service"] ?? attrs["rag_collection"] ?? "";
274
+ }
275
+ if (stepType === "subtask") {
276
+ step.expand_mode = attrs["expand_mode"] ?? "static";
277
+ if (attrs["max_steps"]) {
278
+ const n = parseInt(attrs["max_steps"], 10);
279
+ if (!isNaN(n)) step.max_steps = n;
280
+ }
281
+ }
282
+ return step;
283
+ }
284
+ function buildTree(blocks, stepHeaderRe, grammar) {
285
+ const steps = [];
286
+ let i = 0;
287
+ while (i < blocks.length) {
288
+ const block = blocks[i];
289
+ const { indent, header, body } = block;
290
+ const m = stepHeaderRe.exec(header);
291
+ if (!m) {
292
+ i++;
293
+ continue;
294
+ }
295
+ const stepId = m.groups?.["id"] ?? "";
296
+ const stepName = m.groups?.["name"] ?? "";
297
+ const attrs = parseStepAttrs(body, grammar.attrMap);
298
+ const parenMatch = header.match(/[((]\s*(\w+)\s*[))]/);
299
+ if (parenMatch && !attrs["step_type"]) {
300
+ attrs["step_type"] = parenMatch[1];
301
+ }
302
+ const step = buildStepFromAttrs(stepId, stepName, attrs);
303
+ const childBlocks = [];
304
+ let j = i + 1;
305
+ while (j < blocks.length && blocks[j].indent > indent) {
306
+ childBlocks.push(blocks[j]);
307
+ j++;
308
+ }
309
+ if (childBlocks.length > 0) {
310
+ step.children = buildTree(childBlocks, stepHeaderRe, grammar);
311
+ }
312
+ steps.push(step);
313
+ i = j;
314
+ }
315
+ return steps;
316
+ }
317
+ function parseSpec(markdown, grammar) {
318
+ const g = grammar ?? detectGrammar(markdown);
319
+ const stepHeaderRe = makeStepHeaderRe(g);
320
+ const execLines = extractExecutionSection(markdown, g);
321
+ const blocks = splitIntoStepBlocks(execLines, stepHeaderRe);
322
+ return buildTree(blocks, stepHeaderRe, g);
323
+ }
324
+ function parseFullSpec(markdown, grammar) {
325
+ const g = grammar ?? detectGrammar(markdown);
326
+ const sections = extractSections(markdown);
327
+ const steps = parseSpec(markdown, g);
328
+ const titleMatch = markdown.match(/^#\s+(.+)$/m);
329
+ const title = titleMatch ? titleMatch[1].trim() : "";
330
+ const overviewKey = g.sectionNames["overview"] ?? "\u4EFB\u52A1\u6982\u8FF0";
331
+ const description = sections[overviewKey] ?? "";
332
+ const inputKey = g.sectionNames["input_def"] ?? "\u8F93\u5165\u5B9A\u4E49";
333
+ const outputKey = g.sectionNames["output_format"] ?? "\u8F93\u51FA\u683C\u5F0F";
334
+ return {
335
+ sections,
336
+ steps,
337
+ title,
338
+ description,
339
+ input_format: sections[inputKey] ?? "",
340
+ output_format: sections[outputKey] ?? ""
341
+ };
342
+ }
343
+
344
+ // src/spec-validator.ts
345
+ var VALID_TYPES = /* @__PURE__ */ new Set(["LLM", "call", "loop", "branch", "code", "flow", "subtask"]);
346
+ var VALID_EXPAND_MODES = /* @__PURE__ */ new Set(["static", "dynamic", "think", "seq_think"]);
347
+ var SNAKE_CASE_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
348
+ var INPUT_VAR_RE = /^\s*-\s*`(\w+)`/;
349
+ function checkStructure(steps, sections, grammar) {
350
+ const errors = [];
351
+ const requiredLogical = ["overview", "input_def", "constraints", "execution", "output_format", "input_example"];
352
+ const missing = [];
353
+ for (const logical of requiredLogical) {
354
+ const displayName = grammar.sectionNames[logical];
355
+ if (displayName && !(displayName in sections)) {
356
+ missing.push(displayName);
357
+ }
358
+ }
359
+ if (missing.length > 0) {
360
+ errors.push({
361
+ check: "structure",
362
+ step_id: "",
363
+ message: `Missing required sections: ${missing.join(", ")}`,
364
+ level: "error"
365
+ });
366
+ }
367
+ if (steps.length === 0) {
368
+ errors.push({
369
+ check: "structure",
370
+ step_id: "",
371
+ message: "Execution flow is empty (no steps)",
372
+ level: "error"
373
+ });
374
+ return errors;
375
+ }
376
+ const lastStep = steps[steps.length - 1];
377
+ if (lastStep.step_type !== "flow" || lastStep.flow_action !== "exit") {
378
+ errors.push({
379
+ check: "structure",
380
+ step_id: lastStep.step_id,
381
+ message: `Last top-level step must be flow(action=exit), got ${lastStep.step_type}${lastStep.flow_action ? `:${lastStep.flow_action}` : ""}`,
382
+ level: "error"
383
+ });
384
+ }
385
+ return errors;
386
+ }
387
+ function checkTypes(steps, mode) {
388
+ const errors = [];
389
+ checkTypesRecursive(steps, false, mode, errors);
390
+ return errors;
391
+ }
392
+ function checkTypesRecursive(steps, inLoop, mode, errors) {
393
+ for (const step of steps) {
394
+ if (!VALID_TYPES.has(step.step_type)) {
395
+ errors.push({
396
+ check: "type",
397
+ step_id: step.step_id,
398
+ message: `Invalid step_type "${step.step_type}". Valid: ${[...VALID_TYPES].join(", ")}`,
399
+ level: "error"
400
+ });
401
+ continue;
402
+ }
403
+ if (step.step_type === "flow" && (step.flow_action === "continue" || step.flow_action === "break")) {
404
+ if (!inLoop) {
405
+ errors.push({
406
+ check: "type",
407
+ step_id: step.step_id,
408
+ message: `flow:${step.flow_action} can only appear inside a loop`,
409
+ level: "error"
410
+ });
411
+ }
412
+ }
413
+ if ((step.step_type === "loop" || step.step_type === "branch") && step.children.length === 0) {
414
+ errors.push({
415
+ check: "type",
416
+ step_id: step.step_id,
417
+ message: `${step.step_type} step must have children`,
418
+ level: "error"
419
+ });
420
+ }
421
+ if (step.step_type === "subtask") {
422
+ checkSubtask(step, errors);
423
+ }
424
+ if (mode === "jit" && step.step_type === "loop" && step.loop_mode === "while") {
425
+ if (!step.max_iterations || step.max_iterations <= 0) {
426
+ errors.push({
427
+ check: "type",
428
+ step_id: step.step_id,
429
+ message: "JIT mode: while loop must have max_iterations > 0",
430
+ level: "error"
431
+ });
432
+ }
433
+ }
434
+ const childInLoop = inLoop || step.step_type === "loop";
435
+ if (step.children.length > 0) {
436
+ checkTypesRecursive(step.children, childInLoop, mode, errors);
437
+ }
438
+ }
439
+ }
440
+ function checkSubtask(step, errors) {
441
+ const mode = step.expand_mode ?? "static";
442
+ if (!VALID_EXPAND_MODES.has(mode)) {
443
+ errors.push({
444
+ check: "type",
445
+ step_id: step.step_id,
446
+ message: `Invalid expand_mode "${mode}". Valid: ${[...VALID_EXPAND_MODES].join(", ")}`,
447
+ level: "error"
448
+ });
449
+ return;
450
+ }
451
+ if (mode === "static") {
452
+ if (step.children.length === 0) {
453
+ errors.push({
454
+ check: "type",
455
+ step_id: step.step_id,
456
+ message: "subtask(static) must have children",
457
+ level: "error"
458
+ });
459
+ }
460
+ } else {
461
+ if (step.children.length > 0) {
462
+ errors.push({
463
+ check: "type",
464
+ step_id: step.step_id,
465
+ message: `subtask(${mode}) must NOT have children (generated at runtime)`,
466
+ level: "error"
467
+ });
468
+ }
469
+ if (!step.task) {
470
+ errors.push({
471
+ check: "type",
472
+ step_id: step.step_id,
473
+ message: `subtask(${mode}) must have task description`,
474
+ level: "error"
475
+ });
476
+ }
477
+ if (Object.keys(step.outputs).length === 0) {
478
+ errors.push({
479
+ check: "type",
480
+ step_id: step.step_id,
481
+ message: `subtask(${mode}) must declare outputs`,
482
+ level: "error"
483
+ });
484
+ }
485
+ if (step.max_steps !== void 0 && step.max_steps <= 0) {
486
+ errors.push({
487
+ check: "type",
488
+ step_id: step.step_id,
489
+ message: `subtask(${mode}) max_steps must be > 0`,
490
+ level: "error"
491
+ });
492
+ }
493
+ }
494
+ }
495
+ function checkTree(steps) {
496
+ const errors = [];
497
+ checkTreeRecursive(steps, [], errors);
498
+ return errors;
499
+ }
500
+ function checkTreeRecursive(steps, ancestorLoops, errors) {
501
+ for (const step of steps) {
502
+ if (step.step_type === "flow" && (step.flow_action === "continue" || step.flow_action === "break")) {
503
+ if (step.exit_id && !ancestorLoops.includes(step.exit_id)) {
504
+ errors.push({
505
+ check: "tree",
506
+ step_id: step.step_id,
507
+ message: `flow:${step.flow_action} target "${step.exit_id}" not found in ancestor loops: [${ancestorLoops.join(", ")}]`,
508
+ level: "error"
509
+ });
510
+ }
511
+ }
512
+ if (step.step_type === "branch") {
513
+ if (!step.branch_condition) {
514
+ errors.push({
515
+ check: "tree",
516
+ step_id: step.step_id,
517
+ message: "branch step must have a condition",
518
+ level: "error"
519
+ });
520
+ }
521
+ }
522
+ if (step.step_type === "loop") {
523
+ const hasCollection = !!step.collection;
524
+ const hasCondition = !!step.condition;
525
+ if (!hasCollection && !hasCondition) {
526
+ errors.push({
527
+ check: "tree",
528
+ step_id: step.step_id,
529
+ message: "loop must have either collection (for-each) or condition (while)",
530
+ level: "error"
531
+ });
532
+ }
533
+ }
534
+ const newLoops = step.step_type === "loop" ? [...ancestorLoops, step.step_name] : ancestorLoops;
535
+ if (step.children.length > 0) {
536
+ checkTreeRecursive(step.children, newLoops, errors);
537
+ }
538
+ }
539
+ }
540
+ function checkDataflow(steps, sections, grammar) {
541
+ const errors = [];
542
+ const inputKey = grammar.sectionNames["input_def"] ?? "\u8F93\u5165\u5B9A\u4E49";
543
+ const inputSection = sections[inputKey] ?? "";
544
+ const available = /* @__PURE__ */ new Set();
545
+ for (const line of inputSection.split("\n")) {
546
+ const m = INPUT_VAR_RE.exec(line);
547
+ if (m?.[1]) available.add(m[1]);
548
+ }
549
+ checkDataflowRecursive(steps, available, errors);
550
+ return errors;
551
+ }
552
+ function checkDataflowRecursive(steps, available, errors) {
553
+ for (const step of steps) {
554
+ for (const input of Object.keys(step.inputs)) {
555
+ if (!available.has(input)) {
556
+ errors.push({
557
+ check: "dataflow",
558
+ step_id: step.step_id,
559
+ message: `Input variable "${input}" not produced by prior steps`,
560
+ level: "error"
561
+ });
562
+ }
563
+ }
564
+ if (step.step_type === "loop" && step.loop_mode === "for-each" && step.collection) {
565
+ if (!available.has(step.collection)) {
566
+ errors.push({
567
+ check: "dataflow",
568
+ step_id: step.step_id,
569
+ message: `Loop collection "${step.collection}" not produced by prior steps`,
570
+ level: "error"
571
+ });
572
+ }
573
+ const childAvailable = new Set(available);
574
+ if (step.element_var) childAvailable.add(step.element_var);
575
+ checkDataflowRecursive(step.children, childAvailable, errors);
576
+ } else if (step.children.length > 0) {
577
+ const childAvailable = new Set(available);
578
+ checkDataflowRecursive(step.children, childAvailable, errors);
579
+ for (const child of step.children) {
580
+ for (const out of Object.keys(child.outputs)) {
581
+ available.add(out);
582
+ }
583
+ }
584
+ }
585
+ for (const out of Object.keys(step.outputs)) {
586
+ available.add(out);
587
+ }
588
+ }
589
+ }
590
+ function checkVerifierCoverage(steps) {
591
+ const errors = [];
592
+ checkVerifierRecursive(steps, errors);
593
+ return errors;
594
+ }
595
+ function checkVerifierRecursive(steps, errors) {
596
+ for (const step of steps) {
597
+ if (step.step_type === "LLM" && step.verify_strategy === "none") {
598
+ errors.push({
599
+ check: "verifier",
600
+ step_id: step.step_id,
601
+ message: `LLM step "${step.step_name}" has no explicit verify strategy (default: reverse)`,
602
+ level: "warning"
603
+ });
604
+ }
605
+ if (step.children.length > 0) {
606
+ checkVerifierRecursive(step.children, errors);
607
+ }
608
+ }
609
+ }
610
+ function checkNaming(steps, mode) {
611
+ const errors = [];
612
+ const seen = /* @__PURE__ */ new Set();
613
+ checkNamingRecursive(steps, mode, seen, errors);
614
+ return errors;
615
+ }
616
+ function checkNamingRecursive(steps, mode, seen, errors) {
617
+ for (const step of steps) {
618
+ const name = step.step_name;
619
+ if (!name) continue;
620
+ if (seen.has(name)) {
621
+ errors.push({
622
+ check: "naming",
623
+ step_id: step.step_id,
624
+ message: `Duplicate step_name "${name}"`,
625
+ level: "error"
626
+ });
627
+ }
628
+ seen.add(name);
629
+ if (mode === "aot" && !SNAKE_CASE_RE.test(name)) {
630
+ errors.push({
631
+ check: "naming",
632
+ step_id: step.step_id,
633
+ message: `step_name "${name}" must be snake_case (AOT mode)`,
634
+ level: "error"
635
+ });
636
+ }
637
+ if (step.children.length > 0) {
638
+ checkNamingRecursive(step.children, mode, seen, errors);
639
+ }
640
+ }
641
+ }
642
+ function validateSpec(steps, mode = "jit", sections, grammar) {
643
+ const g = grammar ?? ZH_GRAMMAR;
644
+ const s = sections ?? {};
645
+ return [
646
+ ...checkStructure(steps, s, g),
647
+ ...checkTypes(steps, mode),
648
+ ...checkTree(steps),
649
+ ...checkDataflow(steps, s, g),
650
+ ...checkVerifierCoverage(steps),
651
+ ...checkNaming(steps, mode)
652
+ ];
653
+ }
654
+
655
+ // src/plan-spec.ts
656
+ var VALID_STEP_TYPES = /* @__PURE__ */ new Set(["reason", "act", "check", "decide", "subtask"]);
657
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["decide", "subtask"]);
658
+ var MARKER_TO_STATUS = {
659
+ "[x]": "done",
660
+ "[>]": "active",
661
+ "[!]": "blocked",
662
+ "[-]": "skipped"
663
+ };
664
+ var STATUS_TO_MARKER = {
665
+ done: "[x]",
666
+ active: "[>]",
667
+ blocked: "[!]",
668
+ skipped: "[-]",
669
+ pending: ""
670
+ };
671
+ var STEP_RE = /^(?<indent>\s*)(?<id>[\d.]+)\.\s*(?:(?<marker>\[[ x>!\-]\])\s*)?(?:(?<name>[a-z_]\w*)\s+)?(?:\[(?<type>\w+)\])\s*(?<desc>[^→|]*?)(?:\s*(?:→|->)\s*(?<outputs>[^|]*?))?(?:\s*\|\s*(?<result>.*))?$/;
672
+ var TITLE_RE = /^#\s+Plan:\s*(.+)$/;
673
+ var GOAL_RE = /^Goal:\s*(.+)$/;
674
+ var STATUS_RE = /^Status:\s*(DONE|ABORTED)(?:\s+(.*))?$/;
675
+ var CONSTRAINT_RE = /^-\s+(.+)$/;
676
+ var DETAIL_RE = /^(\s*)>\s?(.*)$/;
677
+ function parsePlan(markdown) {
678
+ const lines = markdown.split("\n");
679
+ let title = "";
680
+ let goal = "";
681
+ const constraints = [];
682
+ let status;
683
+ let abortReason;
684
+ let phase = "header";
685
+ const rawSteps = [];
686
+ let currentStepIdx = -1;
687
+ for (const line of lines) {
688
+ const statusMatch = STATUS_RE.exec(line);
689
+ if (statusMatch) {
690
+ status = statusMatch[1];
691
+ abortReason = statusMatch[2]?.trim() || void 0;
692
+ continue;
693
+ }
694
+ const titleMatch = TITLE_RE.exec(line);
695
+ if (titleMatch) {
696
+ title = titleMatch[1].trim();
697
+ continue;
698
+ }
699
+ const goalMatch = GOAL_RE.exec(line);
700
+ if (goalMatch) {
701
+ goal = goalMatch[1].trim();
702
+ continue;
703
+ }
704
+ if (line.startsWith("Constraints:")) {
705
+ phase = "constraints";
706
+ continue;
707
+ }
708
+ if (line.startsWith("## Steps")) {
709
+ phase = "steps";
710
+ continue;
711
+ }
712
+ if (phase === "constraints") {
713
+ const cm = CONSTRAINT_RE.exec(line);
714
+ if (cm) {
715
+ constraints.push(cm[1].trim());
716
+ } else if (line.trim() === "" || line.startsWith("#")) ;
717
+ continue;
718
+ }
719
+ if (phase === "steps") {
720
+ const sm = STEP_RE.exec(line);
721
+ if (sm) {
722
+ const marker = sm.groups?.["marker"] ?? "";
723
+ const stepStatus = MARKER_TO_STATUS[marker] ?? "pending";
724
+ rawSteps.push({
725
+ indent: (sm.groups?.["indent"] ?? "").length,
726
+ id: sm.groups?.["id"] ?? "",
727
+ status: stepStatus,
728
+ name: sm.groups?.["name"] ?? "",
729
+ type: sm.groups?.["type"] ?? "act",
730
+ description: (sm.groups?.["desc"] ?? "").trim(),
731
+ outputs: (sm.groups?.["outputs"] ?? "").trim(),
732
+ result: (sm.groups?.["result"] ?? "").trim(),
733
+ detail: []
734
+ });
735
+ currentStepIdx = rawSteps.length - 1;
736
+ continue;
737
+ }
738
+ const dm = DETAIL_RE.exec(line);
739
+ if (dm && currentStepIdx >= 0) {
740
+ rawSteps[currentStepIdx].detail.push(dm[2] ?? "");
741
+ continue;
742
+ }
743
+ }
744
+ }
745
+ const steps = buildPlanTree(rawSteps);
746
+ return {
747
+ title,
748
+ goal,
749
+ constraints,
750
+ steps,
751
+ status,
752
+ abort_reason: abortReason
753
+ };
754
+ }
755
+ function buildPlanTree(rawSteps) {
756
+ const result = [];
757
+ let i = 0;
758
+ while (i < rawSteps.length) {
759
+ const raw = rawSteps[i];
760
+ const indent = raw.indent;
761
+ const step = {
762
+ step_id: raw.id,
763
+ step_type: raw.type,
764
+ description: raw.description,
765
+ status: raw.status,
766
+ result: raw.result || void 0,
767
+ outputs: raw.outputs || void 0,
768
+ inputs: void 0,
769
+ detail: raw.detail.length > 0 ? raw.detail.join("\n") : void 0,
770
+ children: []
771
+ };
772
+ if (step.detail) {
773
+ const inputMatch = step.detail.match(/←\s*(.+)/);
774
+ if (inputMatch) {
775
+ step.inputs = inputMatch[1].trim();
776
+ }
777
+ }
778
+ const childRaws = [];
779
+ let j = i + 1;
780
+ while (j < rawSteps.length && rawSteps[j].indent > indent) {
781
+ childRaws.push(rawSteps[j]);
782
+ j++;
783
+ }
784
+ if (childRaws.length > 0) {
785
+ step.children = buildPlanTree(childRaws);
786
+ }
787
+ if (raw.name) {
788
+ step.step_name = raw.name;
789
+ }
790
+ result.push(step);
791
+ i = j;
792
+ }
793
+ return result;
794
+ }
795
+ function serializePlan(plan) {
796
+ const lines = [];
797
+ if (plan.status) {
798
+ lines.push(`Status: ${plan.status}${plan.abort_reason ? ` ${plan.abort_reason}` : ""}`);
799
+ }
800
+ lines.push(`# Plan: ${plan.title}`);
801
+ lines.push(`Goal: ${plan.goal}`);
802
+ if (plan.constraints.length > 0) {
803
+ lines.push("Constraints:");
804
+ for (const c of plan.constraints) {
805
+ lines.push(`- ${c}`);
806
+ }
807
+ }
808
+ lines.push("## Steps");
809
+ serializeSteps(plan.steps, 0, lines);
810
+ return lines.join("\n") + "\n";
811
+ }
812
+ function serializeSteps(steps, depth, lines) {
813
+ const indent = " ".repeat(depth);
814
+ for (const step of steps) {
815
+ const marker = STATUS_TO_MARKER[step.status];
816
+ const markerStr = marker ? `${marker} ` : "";
817
+ const outputStr = step.outputs ? ` \u2192 ${step.outputs}` : "";
818
+ const resultStr = step.result ? ` | ${step.result}` : "";
819
+ lines.push(`${indent}${step.step_id}. ${markerStr}[${step.step_type}] ${step.description}${outputStr}${resultStr}`);
820
+ if (step.detail) {
821
+ for (const dl of step.detail.split("\n")) {
822
+ lines.push(`${indent} > ${dl}`);
823
+ }
824
+ }
825
+ if (step.children.length > 0) {
826
+ serializeSteps(step.children, depth + 1, lines);
827
+ }
828
+ }
829
+ }
830
+ function validatePlan(plan) {
831
+ const errors = [];
832
+ if (!plan.title) {
833
+ errors.push({ step_id: "", message: "Plan must have a title", level: "error" });
834
+ }
835
+ if (!plan.goal) {
836
+ errors.push({ step_id: "", message: "Plan must have a goal", level: "error" });
837
+ }
838
+ if (plan.steps.length === 0) {
839
+ errors.push({ step_id: "", message: "Plan must have at least one step", level: "error" });
840
+ }
841
+ validateSteps(plan.steps, errors);
842
+ return errors;
843
+ }
844
+ function validateSteps(steps, errors) {
845
+ const ids = /* @__PURE__ */ new Set();
846
+ for (const step of steps) {
847
+ if (ids.has(step.step_id)) {
848
+ errors.push({
849
+ step_id: step.step_id,
850
+ message: `Duplicate step_id "${step.step_id}"`,
851
+ level: "error"
852
+ });
853
+ }
854
+ ids.add(step.step_id);
855
+ if (!VALID_STEP_TYPES.has(step.step_type)) {
856
+ errors.push({
857
+ step_id: step.step_id,
858
+ message: `Invalid step_type "${step.step_type}"`,
859
+ level: "error"
860
+ });
861
+ }
862
+ if (CONTAINER_TYPES.has(step.step_type) && step.children.length === 0) {
863
+ errors.push({
864
+ step_id: step.step_id,
865
+ message: `${step.step_type} step should have children`,
866
+ level: "warning"
867
+ });
868
+ }
869
+ if (!CONTAINER_TYPES.has(step.step_type) && step.children.length > 0) {
870
+ errors.push({
871
+ step_id: step.step_id,
872
+ message: `${step.step_type} step should not have children`,
873
+ level: "warning"
874
+ });
875
+ }
876
+ if (step.children.length > 0) {
877
+ validateSteps(step.children, errors);
878
+ }
879
+ }
880
+ }
881
+ function findStep(plan, stepId) {
882
+ return findStepInList(plan.steps, stepId);
883
+ }
884
+ function findStepInList(steps, stepId) {
885
+ for (const step of steps) {
886
+ if (step.step_id === stepId) return step;
887
+ if (step.children.length > 0) {
888
+ const found = findStepInList(step.children, stepId);
889
+ if (found) return found;
890
+ }
891
+ }
892
+ return void 0;
893
+ }
894
+ function countSteps(plan) {
895
+ return countStepList(plan.steps);
896
+ }
897
+ function countStepList(steps) {
898
+ let count = 0;
899
+ for (const step of steps) {
900
+ count++;
901
+ if (step.children.length > 0) {
902
+ count += countStepList(step.children);
903
+ }
904
+ }
905
+ return count;
906
+ }
907
+ function countByStatus(plan) {
908
+ const counts = {
909
+ pending: 0,
910
+ active: 0,
911
+ done: 0,
912
+ blocked: 0,
913
+ skipped: 0
914
+ };
915
+ countStatusRecursive(plan.steps, counts);
916
+ return counts;
917
+ }
918
+ function countStatusRecursive(steps, counts) {
919
+ for (const step of steps) {
920
+ counts[step.status]++;
921
+ if (step.children.length > 0) {
922
+ countStatusRecursive(step.children, counts);
923
+ }
924
+ }
925
+ }
926
+
927
+ // src/format-repair.ts
928
+ var KNOWN_KEYS = /* @__PURE__ */ new Set([
929
+ "fact",
930
+ "logic",
931
+ "explanation",
932
+ "severity",
933
+ "type",
934
+ "location",
935
+ "evidence",
936
+ "verdict",
937
+ "cause",
938
+ "result",
939
+ "description",
940
+ "reason"
941
+ ]);
942
+ function registerKnownKeys(...keys) {
943
+ for (const k of keys) {
944
+ KNOWN_KEYS.add(k);
945
+ }
946
+ }
947
+ function parseResult(raw) {
948
+ if (typeof raw === "object" && raw !== null) {
949
+ if (Array.isArray(raw)) return raw;
950
+ return raw;
951
+ }
952
+ if (typeof raw !== "string") return {};
953
+ const s = raw.trim();
954
+ if (!s) return {};
955
+ const fenceMatch = s.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
956
+ if (fenceMatch) {
957
+ try {
958
+ const parsed = JSON.parse(fenceMatch[1]);
959
+ if (typeof parsed === "object" && parsed !== null) return parsed;
960
+ } catch {
961
+ }
962
+ }
963
+ try {
964
+ const parsed = JSON.parse(s);
965
+ if (typeof parsed === "object" && parsed !== null) return parsed;
966
+ } catch {
967
+ }
968
+ return {};
969
+ }
970
+ function coerceToDict(item) {
971
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
972
+ return item;
973
+ }
974
+ if (typeof item !== "string") return void 0;
975
+ const s = item.trim();
976
+ if (!s.startsWith("{")) return void 0;
977
+ try {
978
+ const parsed = JSON.parse(s);
979
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
980
+ return parsed;
981
+ }
982
+ } catch {
983
+ }
984
+ return parsePseudoJson(s);
985
+ }
986
+ function parsePseudoJson(s) {
987
+ const inner = s.slice(1, -1).trim();
988
+ if (!inner) return {};
989
+ const keyPatterns = [...KNOWN_KEYS].map((k) => escapeRegex2(k)).join("|");
990
+ const splitPattern = new RegExp(`[,\uFF0C]\\s*(?=(${keyPatterns})\\s*:)`);
991
+ const parts = inner.split(splitPattern).filter((p) => p !== void 0);
992
+ const result = {};
993
+ let foundAny = false;
994
+ for (const part of parts) {
995
+ const trimmed = part.trim();
996
+ if (!trimmed) continue;
997
+ const colonIdx = trimmed.indexOf(":");
998
+ if (colonIdx < 0) continue;
999
+ const key = trimmed.slice(0, colonIdx).trim().replace(/^["']|["']$/g, "");
1000
+ if (!key) continue;
1001
+ let value = trimmed.slice(colonIdx + 1).trim();
1002
+ value = value.replace(/[,,]\s*$/, "").replace(/^["']|["']$/g, "");
1003
+ result[key] = value;
1004
+ foundAny = true;
1005
+ }
1006
+ return foundAny ? result : void 0;
1007
+ }
1008
+ function escapeRegex2(s) {
1009
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1010
+ }
1011
+ function coerceToList(item) {
1012
+ if (Array.isArray(item)) return item;
1013
+ if (typeof item !== "string") return void 0;
1014
+ const s = item.trim();
1015
+ if (!s.startsWith("[")) return void 0;
1016
+ try {
1017
+ const parsed = JSON.parse(s);
1018
+ if (Array.isArray(parsed)) return parsed;
1019
+ } catch {
1020
+ }
1021
+ const inner = s.slice(1, -1).trim();
1022
+ if (!inner) return [];
1023
+ const blocks = inner.match(/\{(?:[^{}]*|\{[^{}]*\})*\}/g);
1024
+ if (!blocks || blocks.length === 0) return void 0;
1025
+ const result = [];
1026
+ for (const block of blocks) {
1027
+ const dict = coerceToDict(block);
1028
+ result.push(dict ?? block);
1029
+ }
1030
+ return result;
1031
+ }
1032
+ function repairValue(val) {
1033
+ if (typeof val === "string") {
1034
+ const s = val.trim();
1035
+ if (s.startsWith("{")) {
1036
+ try {
1037
+ const parsed = JSON.parse(s);
1038
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1039
+ return repairValue(parsed);
1040
+ }
1041
+ } catch {
1042
+ }
1043
+ const dict = coerceToDict(s);
1044
+ if (dict) return dict;
1045
+ return val;
1046
+ }
1047
+ if (s.startsWith("[")) {
1048
+ try {
1049
+ const parsed = JSON.parse(s);
1050
+ if (Array.isArray(parsed)) return parsed.map((item) => repairValue(item));
1051
+ } catch {
1052
+ }
1053
+ const list = coerceToList(s);
1054
+ if (list) return list.map((item) => repairValue(item));
1055
+ return val;
1056
+ }
1057
+ return val;
1058
+ }
1059
+ if (Array.isArray(val)) {
1060
+ return val.map((item) => repairValue(item));
1061
+ }
1062
+ if (typeof val === "object" && val !== null) {
1063
+ const result = {};
1064
+ for (const [k, v] of Object.entries(val)) {
1065
+ result[k] = repairValue(v);
1066
+ }
1067
+ return result;
1068
+ }
1069
+ return val;
1070
+ }
1071
+ function detectSerializationResidue(data, path = "") {
1072
+ const issues = [];
1073
+ if (typeof data === "object" && data !== null) {
1074
+ if (Array.isArray(data)) {
1075
+ for (let i = 0; i < data.length; i++) {
1076
+ const itemPath = `${path}[${i}]`;
1077
+ const item = data[i];
1078
+ if (typeof item === "string") {
1079
+ const s = item.trim();
1080
+ if (s.startsWith("{")) issues.push(itemPath);
1081
+ else if (s.startsWith("[")) issues.push(itemPath);
1082
+ } else if (typeof item === "object" && item !== null) {
1083
+ issues.push(...detectSerializationResidue(item, itemPath));
1084
+ }
1085
+ }
1086
+ } else {
1087
+ for (const [k, v] of Object.entries(data)) {
1088
+ const fieldPath = path ? `${path}.${k}` : k;
1089
+ if (typeof v === "string") {
1090
+ const s = v.trim();
1091
+ if (s.startsWith("{")) issues.push(fieldPath);
1092
+ else if (s.startsWith("[")) issues.push(fieldPath);
1093
+ } else if (typeof v === "object" && v !== null) {
1094
+ issues.push(...detectSerializationResidue(v, fieldPath));
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+ return issues;
1100
+ }
1101
+
1102
+ exports.EN_GRAMMAR = EN_GRAMMAR;
1103
+ exports.ZH_GRAMMAR = ZH_GRAMMAR;
1104
+ exports.coerceToDict = coerceToDict;
1105
+ exports.countByStatus = countByStatus;
1106
+ exports.countSteps = countSteps;
1107
+ exports.detectGrammar = detectGrammar;
1108
+ exports.detectSerializationResidue = detectSerializationResidue;
1109
+ exports.findStep = findStep;
1110
+ exports.parseFullSpec = parseFullSpec;
1111
+ exports.parsePlan = parsePlan;
1112
+ exports.parseResult = parseResult;
1113
+ exports.parseSpec = parseSpec;
1114
+ exports.registerKnownKeys = registerKnownKeys;
1115
+ exports.repairValue = repairValue;
1116
+ exports.serializePlan = serializePlan;
1117
+ exports.validatePlan = validatePlan;
1118
+ exports.validateSpec = validateSpec;
1119
+ //# sourceMappingURL=index.cjs.map
1120
+ //# sourceMappingURL=index.cjs.map