@sendbird/actionbook-core 0.7.2
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 +157 -0
- package/dist/index.d.ts +393 -0
- package/dist/index.js +5316 -0
- package/dist/index.js.map +1 -0
- package/dist/types-rEXLrfo_.d.ts +191 -0
- package/dist/ui/index.d.ts +383 -0
- package/dist/ui/index.js +6708 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +115 -0
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# actionbook-core
|
|
2
|
+
|
|
3
|
+
Actionbook 문서의 프레임워크 독립적 코어 라이브러리.
|
|
4
|
+
|
|
5
|
+
Tiptap/ProseMirror에 종속되지 않는 자체 AST 모델, 마크다운 파서/시리얼라이저, Jinja 조건식 평가기, 그리고 구조적 편집 Operation 시스템을 제공합니다.
|
|
6
|
+
|
|
7
|
+
## 왜 필요한가
|
|
8
|
+
|
|
9
|
+
기존 Actionbook 에디터의 문서 모델은 ProseMirror 스키마에 강하게 결합되어 있어서:
|
|
10
|
+
|
|
11
|
+
- 서버 사이드에서 문서를 파싱/검증할 수 없음
|
|
12
|
+
- 에디터를 교체하려면 데이터 모델까지 함께 바꿔야 함
|
|
13
|
+
- Jinja 프리뷰 등 부가 기능이 에디터 DOM에 의존
|
|
14
|
+
|
|
15
|
+
이 라이브러리는 순수 TypeScript로 작성되어 **브라우저, Node.js, 서버리스 환경** 어디서든 동작합니다.
|
|
16
|
+
|
|
17
|
+
## 빠른 시작
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import {
|
|
21
|
+
parseMarkdown,
|
|
22
|
+
serializeToMarkdown,
|
|
23
|
+
fromProseMirrorJSON,
|
|
24
|
+
validate,
|
|
25
|
+
doc, paragraph, text, bold, resourceTag, jumpPoint,
|
|
26
|
+
} from '@sb/actionbook-core';
|
|
27
|
+
|
|
28
|
+
// 마크다운 파싱
|
|
29
|
+
const ast = parseMarkdown('# Title\n\nUse {{tool:t1:My Tool}} at ^START^');
|
|
30
|
+
|
|
31
|
+
// 빌더로 직접 생성
|
|
32
|
+
const manual = doc(
|
|
33
|
+
paragraph(text('Hello '), text('bold', [bold()]), resourceTag('tool', 't1', 'Tool')),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// 스키마 검증
|
|
37
|
+
const errors = validate(ast); // [] 이면 정상
|
|
38
|
+
|
|
39
|
+
// 마크다운 출력
|
|
40
|
+
const md = serializeToMarkdown(ast);
|
|
41
|
+
|
|
42
|
+
// ProseMirror JSON 변환 (기존 editor_data 호환)
|
|
43
|
+
const ast2 = fromProseMirrorJSON(existingEditorData);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 모듈 구조
|
|
47
|
+
|
|
48
|
+
| 모듈 | 설명 |
|
|
49
|
+
|------|------|
|
|
50
|
+
| `ast/` | 타입 정의, 빌더, 순회, 경로 주소 체계 |
|
|
51
|
+
| `schema/` | AST 구조 검증 (URL 프로토콜 allowlist 포함) |
|
|
52
|
+
| `markdown/` | mdast 기반 파서/시리얼라이저 + 커스텀 구문 플러그인 |
|
|
53
|
+
| `json/` | v3 포맷 직렬화 + 레거시 자동 감지 역직렬화 |
|
|
54
|
+
| `compat/` | ProseMirror JSONContent → AST 변환 |
|
|
55
|
+
| `operations/` | 구조적 편집 (Insert/Delete/Replace + undo 지원) |
|
|
56
|
+
| `jinja/` | 순수 텍스트 기반 Jinja 블록 파서 + 안전한 조건식 평가기 |
|
|
57
|
+
| `utils/` | 리소스 태그 추출, 중복 Jump Point 탐지 |
|
|
58
|
+
|
|
59
|
+
## 테스트
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd libs/actionbook-core
|
|
63
|
+
npx vitest run
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
12개 파일, 206개 테스트. 커버리지: AST 빌더/순회, 마크다운 roundtrip, ProseMirror 호환, 스키마 검증, Jinja 평가기 보안 케이스, Operation 타입 안전성, 역직렬화 경계, URL 새니타이제이션.
|
|
67
|
+
|
|
68
|
+
## 보안 설계
|
|
69
|
+
|
|
70
|
+
이 라이브러리는 신뢰되지 않는 입력(사용자가 편집한 마크다운, 외부에서 전달된 editor_data)을 처리하도록 설계되었습니다.
|
|
71
|
+
|
|
72
|
+
| 위협 | 대응 |
|
|
73
|
+
|------|------|
|
|
74
|
+
| Jinja 조건식을 통한 임의 코드 실행 | `new Function` / `eval` 제거. 재귀 하강 파서로 화이트리스트 연산자만 평가 |
|
|
75
|
+
| 악의적 AST 주입 (malformed editor_data) | 모든 역직렬화 경계에서 `validate()` 강제 적용 |
|
|
76
|
+
| `javascript:` URL을 통한 XSS | 파서, PM 변환기, 스키마 검증 3중 차단 (http/https/mailto/tel 만 허용) |
|
|
77
|
+
| 깊게 중첩된 입력으로 stack overflow | 모든 재귀 함수에 MAX_DEPTH 가드 (128) |
|
|
78
|
+
| Operation으로 잘못된 AST 생성 | 부모-자식 타입 매트릭스 검증 + 경로 유효성 체크 |
|
|
79
|
+
|
|
80
|
+
## 현재 한계
|
|
81
|
+
|
|
82
|
+
### 기존 에디터와 아직 통합되지 않음
|
|
83
|
+
|
|
84
|
+
이 라이브러리는 독립 모듈로 동일성 테스트만 완료된 상태입니다. 기존 에디터 코드를 실제로 교체하는 작업은 아직 수행되지 않았으며, 점진적으로 진행할 계획입니다.
|
|
85
|
+
|
|
86
|
+
교체 대상:
|
|
87
|
+
- `converters.ts` → `actionbookToAST()`
|
|
88
|
+
- `processResourceTags.ts` → 파서가 직접 노드 생성하므로 삭제 가능
|
|
89
|
+
- `editorConverter.ts` → `parseMarkdown()` + `serializeToMarkdown()`
|
|
90
|
+
- `JinjaPreview/` → `jinja/` 모듈
|
|
91
|
+
|
|
92
|
+
### 마크다운 파서 간 미세한 차이
|
|
93
|
+
|
|
94
|
+
기존: tiptap-markdown (markdown-it 기반) → 이 라이브러리: mdast (micromark 기반)
|
|
95
|
+
|
|
96
|
+
대부분의 마크다운에서 동일한 결과를 내지만, edge case에서 차이가 발생할 수 있습니다:
|
|
97
|
+
|
|
98
|
+
- **Loose/tight list 판단**: 빈 줄이 포함된 리스트의 처리 방식이 다를 수 있음
|
|
99
|
+
- **리스트 들여쓰기**: 2칸 vs 4칸 인식 차이
|
|
100
|
+
- **HTML 인라인**: 이 라이브러리는 HTML을 지원하지 않음 (설계 의도)
|
|
101
|
+
|
|
102
|
+
**완화 방안**: 실제 프로덕션 데이터에서 추출한 마크다운 fixture를 추가하여 차이를 조기에 발견해야 합니다.
|
|
103
|
+
|
|
104
|
+
### Jinja evaluator 제약
|
|
105
|
+
|
|
106
|
+
안전성을 위해 재귀 하강 파서를 사용하므로, Jinja2의 전체 표현식 문법을 지원하지 않습니다.
|
|
107
|
+
|
|
108
|
+
**지원됨**: `==`, `!=`, `<`, `>`, `<=`, `>=`, `and`, `or`, `not`, `in`, `is`, `is not`, 괄호, 문자열/숫자/불리언/None 리터럴, 점 표기법 변수
|
|
109
|
+
|
|
110
|
+
**미지원**:
|
|
111
|
+
- 필터 (`value | upper`, `items | length`)
|
|
112
|
+
- 함수 호출 (`len(items)`, `range(10)`)
|
|
113
|
+
- 리스트/딕셔너리 리터럴 (`[1, 2]`, `{"k": "v"}`)
|
|
114
|
+
- 삼항 (`"a" if cond else "b"`)
|
|
115
|
+
- `not in` 연산자
|
|
116
|
+
- 슬라이싱 (`items[0]`)
|
|
117
|
+
|
|
118
|
+
실제 Actionbook에서 사용 중인 조건식이 이 범위를 넘는다면 evaluator 확장이 필요합니다.
|
|
119
|
+
|
|
120
|
+
### Operations 모듈은 기반만 구현
|
|
121
|
+
|
|
122
|
+
- **노드 단위 편집만 가능**: 텍스트 노드 내부의 문자 레벨 편집(split/merge)은 미지원
|
|
123
|
+
- **동시성 미지원**: CRDT/OT 변환 로직 없음. `NodePath` + `Transaction` 구조가 확장 포인트로 설계되어 있지만 실제 conflict resolution은 없음
|
|
124
|
+
- **빈 컨테이너 허용**: 모든 listItem을 삭제한 후 빈 bulletList가 남는 등의 의미적 무결성은 검증하지 않음 (schema validation에서는 잡힘)
|
|
125
|
+
|
|
126
|
+
### 번들 크기 미검증
|
|
127
|
+
|
|
128
|
+
`micromark` + `mdast-util` 생태계 의존성이 추가됨. 루트 프로젝트가 이미 `remark-gfm`을 쓰고 있어 중복은 적을 것으로 예상하지만, 실제 tree-shaking 후 번들 크기 측정은 아직 하지 않았습니다.
|
|
129
|
+
|
|
130
|
+
## 향후 계획
|
|
131
|
+
|
|
132
|
+
### 단기 (통합 단계)
|
|
133
|
+
|
|
134
|
+
1. **프로덕션 fixture 확보**: 실제 고객 Actionbook 데이터에서 마크다운 + PM JSON 쌍을 추출하여 동일성 테스트 보강
|
|
135
|
+
2. **기존 코드 점진적 교체**: 가장 리스크가 낮은 읽기 전용 경로(프리뷰, 내보내기)부터 이 라이브러리로 전환
|
|
136
|
+
3. **번들 크기 측정**: Vite 빌드에서의 실제 기여도 확인
|
|
137
|
+
|
|
138
|
+
### 중기 (에디터 독립화)
|
|
139
|
+
|
|
140
|
+
4. **actionbook-renderer**: 자체 AST를 입력으로 받는 React 읽기 전용 렌더러. 프리뷰에 먼저 적용
|
|
141
|
+
5. **Jinja evaluator 확장**: 필터, `not in`, 리스트 리터럴 등 실제 사용되는 패턴 추가
|
|
142
|
+
6. **서버 사이드 검증**: Node.js 서버에서 마크다운 파싱/Jinja 변수 추출 수행
|
|
143
|
+
|
|
144
|
+
### 장기 (에디터 교체)
|
|
145
|
+
|
|
146
|
+
7. **actionbook-editor**: 자체 AST + Operation 기반의 편집 가능한 에디터
|
|
147
|
+
8. **CRDT 협업 편집**: Operation 모델 위에 Yjs/Automerge 어댑터
|
|
148
|
+
9. **History 시스템**: `Transaction` + `invertOperation()` 기반 undo/redo 스택
|
|
149
|
+
|
|
150
|
+
## 리스크
|
|
151
|
+
|
|
152
|
+
| 리스크 | 가능성 | 영향 | 완화 |
|
|
153
|
+
|--------|--------|------|------|
|
|
154
|
+
| remark↔markdown-it 파싱 차이로 데이터 깨짐 | 중간 | 높음 | 프로덕션 fixture 테스트, normalize 함수, 이중 실행 비교 |
|
|
155
|
+
| Jinja evaluator가 실제 사용 패턴을 커버 못함 | 낮음 | 중간 | 실사용 조건식 수집 → tokenizer/parser 확장 |
|
|
156
|
+
| 번들 크기 증가 | 낮음 | 낮음 | tree-shaking 확인, lazy import 검토 |
|
|
157
|
+
| `^id^` 와 기존 에디터의 caret 충돌 | 낮음 | 낮음 | 알파벳+언더스코어 제한으로 범위 축소 |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { B as BlockNode, a as BlockquoteNode, b as BoldMark, L as ListItemNode, c as BulletListNode, C as CodeMark, D as DocumentNode, H as HardBreakNode, I as InlineNode, d as HeadingNode, e as HorizontalRuleNode, f as ItalicMark, J as JinjaIfBranch, g as JinjaIfBlockNode, h as JinjaIfInlineNode, i as JumpPointNode, j as LinkMark, O as OrderedListNode, P as ParagraphNode, R as ResourceTagType, k as ResourceTagNode, S as StrikethroughMark, T as TableRowNode, l as TableNode, m as TableCellNode, M as Mark, n as TextNode, U as UnderlineMark, A as AstNode, N as NodePath, o as LintRule, p as LintContext, q as LintResult, r as LlmLintRule, s as LlmCompletionEndpoint, t as LintSection } from './types-rEXLrfo_.js';
|
|
2
|
+
export { u as JSONContent, v as JUMP_POINT_ID_PATTERN, w as LintSeverity, x as RESOURCE_TAG_TYPES, y as fromProseMirrorJSON } from './types-rEXLrfo_.js';
|
|
3
|
+
import { Root } from 'mdast';
|
|
4
|
+
|
|
5
|
+
declare const bold: () => BoldMark;
|
|
6
|
+
declare const italic: () => ItalicMark;
|
|
7
|
+
declare const underline: () => UnderlineMark;
|
|
8
|
+
declare const strikethrough: () => StrikethroughMark;
|
|
9
|
+
declare const code: () => CodeMark;
|
|
10
|
+
declare const link: (href: string, title?: string) => LinkMark;
|
|
11
|
+
declare const text: (t: string, marks?: Mark[]) => TextNode;
|
|
12
|
+
declare const resourceTag: (tagType: ResourceTagType, resourceId: string, displayText: string) => ResourceTagNode;
|
|
13
|
+
declare const jumpPoint: (id: string) => JumpPointNode;
|
|
14
|
+
declare const hardBreak: () => HardBreakNode;
|
|
15
|
+
declare const paragraph: (...content: InlineNode[]) => ParagraphNode;
|
|
16
|
+
declare const heading: (level: 1 | 2 | 3 | 4 | 5 | 6, ...content: InlineNode[]) => HeadingNode;
|
|
17
|
+
declare const bulletList: (...items: ListItemNode[]) => BulletListNode;
|
|
18
|
+
declare const orderedList: (start: number, ...items: ListItemNode[]) => OrderedListNode;
|
|
19
|
+
declare function listItem(...content: BlockNode[]): ListItemNode;
|
|
20
|
+
declare function listItem(checked: boolean | null, ...content: BlockNode[]): ListItemNode;
|
|
21
|
+
declare const todoItem: (checked: boolean, ...content: BlockNode[]) => ListItemNode;
|
|
22
|
+
declare const blockquote: (...content: BlockNode[]) => BlockquoteNode;
|
|
23
|
+
declare const horizontalRule: () => HorizontalRuleNode;
|
|
24
|
+
declare const tableCell: (header: boolean, ...content: InlineNode[]) => TableCellNode;
|
|
25
|
+
declare const tableRow: (...cells: TableCellNode[]) => TableRowNode;
|
|
26
|
+
declare const table: (...rows: TableRowNode[]) => TableNode;
|
|
27
|
+
declare function jinjaBranch<T extends InlineNode | BlockNode>(branchType: 'if' | 'elif' | 'else', condition: string | undefined, ...content: T[]): JinjaIfBranch<T>;
|
|
28
|
+
declare const jinjaIfBlock: (...branches: JinjaIfBranch<BlockNode>[]) => JinjaIfBlockNode;
|
|
29
|
+
declare const jinjaIfInline: (...branches: JinjaIfBranch<InlineNode>[]) => JinjaIfInlineNode;
|
|
30
|
+
declare const doc: (...content: BlockNode[]) => DocumentNode;
|
|
31
|
+
|
|
32
|
+
interface VisitOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Whether to compute and pass a NodePath to each callback invocation.
|
|
35
|
+
* Defaults to true for backward compatibility.
|
|
36
|
+
* Set to false for performance-critical traversals that don't need path info.
|
|
37
|
+
*/
|
|
38
|
+
trackPath?: boolean;
|
|
39
|
+
}
|
|
40
|
+
declare function visit(node: AstNode, callback: (node: AstNode, parent?: AstNode, path?: NodePath) => void, options?: VisitOptions): void;
|
|
41
|
+
declare function map(node: AstNode, fn: (node: AstNode) => AstNode, _depth?: number): AstNode;
|
|
42
|
+
declare function textContent(node: AstNode, _depth?: number): string;
|
|
43
|
+
declare function findAll(node: AstNode, predicate: (n: AstNode) => boolean): AstNode[];
|
|
44
|
+
declare function nodeAtPath(root: DocumentNode, path: NodePath): AstNode | undefined;
|
|
45
|
+
|
|
46
|
+
declare function isBlock(node: AstNode): node is BlockNode;
|
|
47
|
+
declare function isInline(node: AstNode): node is InlineNode;
|
|
48
|
+
declare function isTextNode(node: AstNode): node is TextNode;
|
|
49
|
+
declare function isDocument(node: AstNode): node is DocumentNode;
|
|
50
|
+
|
|
51
|
+
declare function parentPath(path: NodePath): NodePath;
|
|
52
|
+
declare function pathToString(path: NodePath): string;
|
|
53
|
+
declare function pathFromString(str: string): NodePath;
|
|
54
|
+
declare function comparePaths(a: NodePath, b: NodePath): number;
|
|
55
|
+
|
|
56
|
+
type ValidationError = {
|
|
57
|
+
path: string;
|
|
58
|
+
message: string;
|
|
59
|
+
};
|
|
60
|
+
declare function validate(doc: DocumentNode): ValidationError[];
|
|
61
|
+
|
|
62
|
+
interface ParseMarkdownOptions {
|
|
63
|
+
jinjaNodes?: boolean;
|
|
64
|
+
}
|
|
65
|
+
declare function parseMarkdown(markdown: string, options?: ParseMarkdownOptions): DocumentNode;
|
|
66
|
+
|
|
67
|
+
declare function serializeToMarkdown(doc: DocumentNode): string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Serialize a list of AST nodes to markdown string (for clipboard).
|
|
71
|
+
*/
|
|
72
|
+
declare function serializeFragment(nodes: AstNode[]): string;
|
|
73
|
+
/**
|
|
74
|
+
* Parse a markdown string into a fragment (for clipboard paste).
|
|
75
|
+
* Returns block nodes by default; if the markdown is a single paragraph,
|
|
76
|
+
* returns its inline children.
|
|
77
|
+
*/
|
|
78
|
+
declare function parseFragment(markdown: string): InlineNode[] | BlockNode[];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Bidirectional conversion between mdast and ActionbookAST.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
declare function fromMdast(root: Root): DocumentNode;
|
|
85
|
+
declare function toMdast(doc: DocumentNode): Root;
|
|
86
|
+
|
|
87
|
+
type ActionbookJSON = {
|
|
88
|
+
version: 3;
|
|
89
|
+
ast: DocumentNode;
|
|
90
|
+
};
|
|
91
|
+
declare function serializeToJSON(doc: DocumentNode): string;
|
|
92
|
+
|
|
93
|
+
declare class DeserializationError extends Error {
|
|
94
|
+
readonly validationErrors?: Array<{
|
|
95
|
+
path: string;
|
|
96
|
+
message: string;
|
|
97
|
+
}> | undefined;
|
|
98
|
+
constructor(message: string, validationErrors?: Array<{
|
|
99
|
+
path: string;
|
|
100
|
+
message: string;
|
|
101
|
+
}> | undefined);
|
|
102
|
+
}
|
|
103
|
+
declare function deserializeFromJSON(json: string): DocumentNode;
|
|
104
|
+
|
|
105
|
+
type InsertOp = {
|
|
106
|
+
readonly type: 'insert';
|
|
107
|
+
readonly path: NodePath;
|
|
108
|
+
readonly node: AstNode;
|
|
109
|
+
};
|
|
110
|
+
type DeleteOp = {
|
|
111
|
+
readonly type: 'delete';
|
|
112
|
+
readonly path: NodePath;
|
|
113
|
+
};
|
|
114
|
+
type ReplaceOp = {
|
|
115
|
+
readonly type: 'replace';
|
|
116
|
+
readonly path: NodePath;
|
|
117
|
+
readonly node: AstNode;
|
|
118
|
+
};
|
|
119
|
+
type Operation = InsertOp | DeleteOp | ReplaceOp;
|
|
120
|
+
type Transaction = {
|
|
121
|
+
readonly operations: readonly Operation[];
|
|
122
|
+
readonly timestamp: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
declare class OperationError extends Error {
|
|
126
|
+
constructor(message: string);
|
|
127
|
+
}
|
|
128
|
+
declare function applyOperation(doc: DocumentNode, op: Operation): DocumentNode;
|
|
129
|
+
declare function applyTransaction(doc: DocumentNode, tx: Transaction): DocumentNode;
|
|
130
|
+
declare function invertOperation(doc: DocumentNode, op: Operation): Operation;
|
|
131
|
+
|
|
132
|
+
interface JinjaBlock {
|
|
133
|
+
type: 'if' | 'elif' | 'else' | 'endif';
|
|
134
|
+
condition?: string;
|
|
135
|
+
startIndex: number;
|
|
136
|
+
endIndex: number;
|
|
137
|
+
raw: string;
|
|
138
|
+
}
|
|
139
|
+
interface JinjaIfStructure {
|
|
140
|
+
id: string;
|
|
141
|
+
ifBlock: JinjaBlock;
|
|
142
|
+
elifBlocks: JinjaBlock[];
|
|
143
|
+
elseBlock?: JinjaBlock;
|
|
144
|
+
endifBlock: JinjaBlock;
|
|
145
|
+
contentRanges: Array<{
|
|
146
|
+
type: 'if' | 'elif' | 'else';
|
|
147
|
+
condition?: string;
|
|
148
|
+
startIndex: number;
|
|
149
|
+
endIndex: number;
|
|
150
|
+
}>;
|
|
151
|
+
}
|
|
152
|
+
interface Variable {
|
|
153
|
+
name: string;
|
|
154
|
+
type?: 'string' | 'number' | 'boolean';
|
|
155
|
+
}
|
|
156
|
+
interface VariableValue {
|
|
157
|
+
value: string | number | boolean;
|
|
158
|
+
type: 'string' | 'number' | 'boolean';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Pure text-based Jinja block scanner (no ProseMirror dependency).
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Scan all Jinja blocks from plain text.
|
|
167
|
+
*
|
|
168
|
+
* @param text - Plain text content
|
|
169
|
+
* @returns All Jinja blocks sorted by position
|
|
170
|
+
*/
|
|
171
|
+
declare function scanJinjaBlocks(text: string): JinjaBlock[];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Structure scanned blocks into if~endif structures using stack-based approach.
|
|
175
|
+
*/
|
|
176
|
+
declare function structureJinjaBlocks(blocks: JinjaBlock[]): JinjaIfStructure[];
|
|
177
|
+
|
|
178
|
+
interface JinjaVisualBranch {
|
|
179
|
+
type: 'if' | 'elif' | 'else';
|
|
180
|
+
condition?: string;
|
|
181
|
+
blockStartIndex: number;
|
|
182
|
+
blockEndIndex: number;
|
|
183
|
+
tagBlockIndex: number;
|
|
184
|
+
}
|
|
185
|
+
interface JinjaVisualStructure {
|
|
186
|
+
id: string;
|
|
187
|
+
branches: JinjaVisualBranch[];
|
|
188
|
+
ifTagBlockIndex: number;
|
|
189
|
+
endifTagBlockIndex: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Analyze a DocumentNode for Jinja if/elif/else/endif structures
|
|
193
|
+
* and map them to block-level indices.
|
|
194
|
+
*/
|
|
195
|
+
declare function analyzeJinjaBlocks(doc: DocumentNode): JinjaVisualStructure[];
|
|
196
|
+
|
|
197
|
+
declare function extractVariables(condition: string): Variable[];
|
|
198
|
+
declare function collectAllVariables(structures: JinjaIfStructure[]): Variable[];
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Safe Jinja condition evaluator using recursive descent parsing.
|
|
202
|
+
*
|
|
203
|
+
* Supports:
|
|
204
|
+
* Comparison: ==, !=, <, >, <=, >=
|
|
205
|
+
* Logical: and, or, not
|
|
206
|
+
* Identity: is, is not
|
|
207
|
+
* Membership: in (value in identifier)
|
|
208
|
+
* Literals: strings, numbers, booleans (True/False/true/false), None/null
|
|
209
|
+
* Variables: dot-notation identifiers (e.g. context.planName)
|
|
210
|
+
* Grouping: parentheses
|
|
211
|
+
*
|
|
212
|
+
* NO arbitrary code execution — only whitelisted operators and literal types.
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Safely evaluate a Jinja conditional expression.
|
|
217
|
+
*
|
|
218
|
+
* Uses a recursive-descent parser with a whitelist of allowed operators.
|
|
219
|
+
* No `eval`, `new Function`, or arbitrary code execution.
|
|
220
|
+
*
|
|
221
|
+
* @returns true/false if evaluation succeeded, null if parsing/evaluation failed
|
|
222
|
+
*/
|
|
223
|
+
declare function evaluateCondition(condition: string, variables: Map<string, VariableValue>): boolean | null;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Evaluate Jinja if-block AST nodes by selecting the first truthy branch.
|
|
227
|
+
* Post-processes adjacent same-type lists to merge and renumber.
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Evaluate all jinjaIfBlock and jinjaIfInline nodes in a document,
|
|
232
|
+
* replacing them with the content of the first truthy branch.
|
|
233
|
+
*/
|
|
234
|
+
declare function evaluateJinjaNodes(doc: DocumentNode, variables: Map<string, VariableValue>): DocumentNode;
|
|
235
|
+
|
|
236
|
+
declare function lint(doc: DocumentNode, rules: readonly LintRule[], ctx?: LintContext): LintResult[];
|
|
237
|
+
|
|
238
|
+
declare const defaultRules: readonly LintRule[];
|
|
239
|
+
|
|
240
|
+
interface LintAsyncOptions {
|
|
241
|
+
rules?: readonly LintRule[];
|
|
242
|
+
llmRules?: readonly LlmLintRule[];
|
|
243
|
+
llmEndpoint?: LlmCompletionEndpoint;
|
|
244
|
+
ctx?: LintContext;
|
|
245
|
+
signal?: AbortSignal;
|
|
246
|
+
onProgress?: (completed: number, total: number) => void;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Runs both static lint rules and LLM-based lint rules.
|
|
250
|
+
* Static rules run synchronously first, then LLM rules run per-section sequentially.
|
|
251
|
+
* Returns all results combined.
|
|
252
|
+
*/
|
|
253
|
+
declare function lintAsync(doc: DocumentNode, options: LintAsyncOptions): Promise<LintResult[]>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Splits a document into sections by heading boundaries.
|
|
257
|
+
* Each section contains the heading text, its path in the heading hierarchy,
|
|
258
|
+
* the full text content, and the block index range.
|
|
259
|
+
*/
|
|
260
|
+
declare function chunkByHeading(doc: DocumentNode): LintSection[];
|
|
261
|
+
|
|
262
|
+
declare const defaultLlmRules: readonly LlmLintRule[];
|
|
263
|
+
|
|
264
|
+
declare function extractResourceTags(doc: DocumentNode): ResourceTagNode[];
|
|
265
|
+
|
|
266
|
+
declare function findDuplicateJumpPoints(doc: DocumentNode): string[];
|
|
267
|
+
|
|
268
|
+
type TreeNodeType = 'section_entry' | 'jinja_condition' | 'conditional' | 'content' | 'end';
|
|
269
|
+
type TreeNodeClassification = 'agent_speech' | 'tool_call_only' | 'mixed_with_speech' | 'structural' | 'internal_action' | 'user_action' | 'agent_internal';
|
|
270
|
+
interface TreeNodeLine {
|
|
271
|
+
blockIndex: number;
|
|
272
|
+
content: string;
|
|
273
|
+
hasToolCall: boolean;
|
|
274
|
+
toolIds: string[];
|
|
275
|
+
hasJumpPoint: boolean;
|
|
276
|
+
jumpTargets: string[];
|
|
277
|
+
hasHandoff: boolean;
|
|
278
|
+
}
|
|
279
|
+
interface TreeNode {
|
|
280
|
+
id: string;
|
|
281
|
+
type: TreeNodeType;
|
|
282
|
+
blockRange: {
|
|
283
|
+
start: number;
|
|
284
|
+
end: number;
|
|
285
|
+
};
|
|
286
|
+
lines: TreeNodeLine[];
|
|
287
|
+
summary: string;
|
|
288
|
+
children: string[];
|
|
289
|
+
depth: number;
|
|
290
|
+
classification?: TreeNodeClassification;
|
|
291
|
+
conditionText?: string;
|
|
292
|
+
isJinja?: boolean;
|
|
293
|
+
branchType?: 'if' | 'elif' | 'else';
|
|
294
|
+
gotoTarget?: string;
|
|
295
|
+
hasGotoEdge?: boolean;
|
|
296
|
+
isContainer?: boolean;
|
|
297
|
+
}
|
|
298
|
+
interface TreeSection {
|
|
299
|
+
headingBlockIndex: number;
|
|
300
|
+
rootNodeId: string | null;
|
|
301
|
+
ancestorConditions: Array<{
|
|
302
|
+
condition: string;
|
|
303
|
+
branchType: string;
|
|
304
|
+
blockIndex: number;
|
|
305
|
+
}>;
|
|
306
|
+
}
|
|
307
|
+
interface GotoEdge {
|
|
308
|
+
fromNodeId: string;
|
|
309
|
+
toSection: string;
|
|
310
|
+
}
|
|
311
|
+
interface DecisionTree {
|
|
312
|
+
rootNodeId: string | null;
|
|
313
|
+
nodes: Record<string, TreeNode>;
|
|
314
|
+
sections: Record<string, TreeSection>;
|
|
315
|
+
gotoEdges: GotoEdge[];
|
|
316
|
+
statistics: {
|
|
317
|
+
totalNodes: number;
|
|
318
|
+
totalPaths: number;
|
|
319
|
+
maxDepth: number;
|
|
320
|
+
branchPoints: number;
|
|
321
|
+
sectionCount: number;
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
interface TreePath {
|
|
325
|
+
nodeIds: string[];
|
|
326
|
+
sectionSequence: string[];
|
|
327
|
+
branchDecisions: BranchDecision[];
|
|
328
|
+
contextRequirements: Record<string, string>;
|
|
329
|
+
terminationReason: 'end' | 'goto' | 'cycle' | 'leaf';
|
|
330
|
+
gotoTarget?: string;
|
|
331
|
+
cycleNodeId?: string;
|
|
332
|
+
}
|
|
333
|
+
interface BranchDecision {
|
|
334
|
+
nodeId: string;
|
|
335
|
+
branchType: string;
|
|
336
|
+
condition: string;
|
|
337
|
+
blockIndex: number;
|
|
338
|
+
contextRequirements?: Record<string, string>;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Tree Generator for Actionbook Decision Trees
|
|
343
|
+
*
|
|
344
|
+
* Converts an actionbook-core DocumentNode AST into a decision tree
|
|
345
|
+
* by exhaustively traversing all branches (Jinja and Conditional).
|
|
346
|
+
*
|
|
347
|
+
* Port of aidebugger's treeGenerator.js, adapted for the actionbook-core
|
|
348
|
+
* AST model (block-level nodes instead of line-level parsed objects).
|
|
349
|
+
*/
|
|
350
|
+
|
|
351
|
+
declare function generateDecisionTree(doc: DocumentNode): DecisionTree;
|
|
352
|
+
|
|
353
|
+
declare function enumeratePaths(tree: DecisionTree, options?: {
|
|
354
|
+
startSection?: string;
|
|
355
|
+
followGotoEdges?: boolean;
|
|
356
|
+
}): TreePath[];
|
|
357
|
+
interface EnumeratePathsIterOptions {
|
|
358
|
+
startSection?: string;
|
|
359
|
+
followGotoEdges?: boolean;
|
|
360
|
+
/** Stop after yielding this many paths (memory guard). */
|
|
361
|
+
maxPaths?: number;
|
|
362
|
+
/** Skip paths deeper than this node count. */
|
|
363
|
+
maxDepth?: number;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Generator-based streaming API for path enumeration.
|
|
367
|
+
* Yields one TreePath at a time without materializing the full list.
|
|
368
|
+
* Uses an explicit stack instead of recursion to avoid call-stack overflow
|
|
369
|
+
* on deep trees.
|
|
370
|
+
*/
|
|
371
|
+
declare function enumeratePathsIter(tree: DecisionTree, options?: EnumeratePathsIterOptions): IterableIterator<TreePath>;
|
|
372
|
+
declare function enumeratePathsBySection(tree: DecisionTree): Record<string, TreePath[]>;
|
|
373
|
+
|
|
374
|
+
interface GotoEntryResolution {
|
|
375
|
+
entryNodeIds: string[];
|
|
376
|
+
targetSection: string;
|
|
377
|
+
resolved: boolean;
|
|
378
|
+
}
|
|
379
|
+
declare function resolveGotoEntryPath(tree: DecisionTree, sectionName: string, context: Map<string, VariableValue>): GotoEntryResolution | null;
|
|
380
|
+
|
|
381
|
+
type LogicV2 = {
|
|
382
|
+
version: 2;
|
|
383
|
+
instruction: string;
|
|
384
|
+
editor_data?: string;
|
|
385
|
+
};
|
|
386
|
+
declare class ActionbookConversionError extends Error {
|
|
387
|
+
constructor(message: string);
|
|
388
|
+
}
|
|
389
|
+
declare function actionbookToAST(manual: {
|
|
390
|
+
logic: LogicV2;
|
|
391
|
+
}): DocumentNode;
|
|
392
|
+
|
|
393
|
+
export { ActionbookConversionError, type ActionbookJSON, AstNode, BlockNode, BlockquoteNode, BoldMark, type BranchDecision, BulletListNode, CodeMark, type DecisionTree, type DeleteOp, DeserializationError, DocumentNode, type EnumeratePathsIterOptions, type GotoEdge, type GotoEntryResolution, HardBreakNode, HeadingNode, HorizontalRuleNode, InlineNode, type InsertOp, ItalicMark, type JinjaBlock, JinjaIfBlockNode, JinjaIfBranch, JinjaIfInlineNode, type JinjaIfStructure, type JinjaVisualBranch, type JinjaVisualStructure, JumpPointNode, LinkMark, type LintAsyncOptions, LintContext, LintResult, LintRule, LintSection, ListItemNode, LlmCompletionEndpoint, LlmLintRule, Mark, NodePath, type Operation, OperationError, OrderedListNode, ParagraphNode, type ParseMarkdownOptions, type ReplaceOp, ResourceTagNode, ResourceTagType, StrikethroughMark, TableCellNode, TableNode, TableRowNode, TextNode, type Transaction, type TreeNode, type TreeNodeClassification, type TreeNodeLine, type TreeNodeType, type TreePath, type TreeSection, UnderlineMark, type ValidationError, type Variable, type VariableValue, type VisitOptions, actionbookToAST, analyzeJinjaBlocks, applyOperation, applyTransaction, blockquote, bold, bulletList, chunkByHeading, code, collectAllVariables, comparePaths, defaultLlmRules, defaultRules, deserializeFromJSON, doc, enumeratePaths, enumeratePathsBySection, enumeratePathsIter, evaluateCondition, evaluateJinjaNodes, extractResourceTags, extractVariables, findAll, findDuplicateJumpPoints, fromMdast, generateDecisionTree, hardBreak, heading, horizontalRule, invertOperation, isBlock, isDocument, isInline, isTextNode, italic, jinjaBranch, jinjaIfBlock, jinjaIfInline, jumpPoint, link, lint, lintAsync, listItem, map, nodeAtPath, orderedList, paragraph, parentPath, parseFragment, parseMarkdown, pathFromString, pathToString, resolveGotoEntryPath, resourceTag, scanJinjaBlocks, serializeFragment, serializeToJSON, serializeToMarkdown, strikethrough, structureJinjaBlocks, table, tableCell, tableRow, text, textContent, toMdast, todoItem, underline, validate, visit };
|