@viengut/vgq-sdk 0.1.0 → 0.2.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/engine.d.ts CHANGED
@@ -11,12 +11,15 @@ export declare function topoOrder(def: FormDefinition): {
11
11
  export interface ActiveState {
12
12
  /** Id các Question đang hoạt động. */
13
13
  activeIds: Set<string>;
14
+ /** Id các Category đang hiện (điều kiện của chính nó và mọi tổ tiên đều đúng). */
15
+ activeCategoryIds: Set<string>;
14
16
  /** Giá trị hiệu lực theo slug: chỉ gồm giá trị của Question đang hoạt động. */
15
17
  effectiveValues: ResponseValues;
16
18
  }
17
19
  /**
18
20
  * Đánh giá Enable Condition toàn Form theo thứ tự topo. Giá trị của Question
19
21
  * không hoạt động bị loại khỏi giá trị hiệu lực nên chuỗi phụ thuộc tắt theo.
22
+ * Question trong Category tắt cũng không hoạt động.
20
23
  */
21
24
  export declare function computeActive(def: FormDefinition, values: ResponseValues): ActiveState;
22
25
  export declare function isAnswered(value: JsonValue | undefined): boolean;
package/dist/engine.js CHANGED
@@ -12,8 +12,27 @@ export function allQuestions(def) {
12
12
  walk(def.categories);
13
13
  return out;
14
14
  }
15
- function conditionRefs(question) {
16
- return (question.enable_condition?.any ?? []).flatMap((group) => group.all.map((condition) => condition.question_id));
15
+ function conditionRefs(cond) {
16
+ return (cond?.any ?? []).flatMap((group) => group.all.map((condition) => condition.question_id));
17
+ }
18
+ /**
19
+ * Phụ thuộc Enable Condition của từng Question: refs của chính nó cộng refs
20
+ * điều kiện của mọi Category tổ tiên (Category tắt kéo Question tắt theo,
21
+ * nên thứ tự topo và phát hiện chu trình phủ luôn điều kiện cấp Category).
22
+ */
23
+ function dependencyMap(def) {
24
+ const out = new Map();
25
+ const walk = (categories, inherited) => {
26
+ for (const category of categories) {
27
+ const refs = [...inherited, ...conditionRefs(category.enable_condition)];
28
+ for (const q of category.questions) {
29
+ out.set(q.id, [...refs, ...conditionRefs(q.enable_condition)]);
30
+ }
31
+ walk(category.children ?? [], refs);
32
+ }
33
+ };
34
+ walk(def.categories, []);
35
+ return out;
17
36
  }
18
37
  /**
19
38
  * Sắp xếp topo theo phụ thuộc Enable Condition (Kahn).
@@ -22,10 +41,11 @@ function conditionRefs(question) {
22
41
  export function topoOrder(def) {
23
42
  const questions = allQuestions(def);
24
43
  const known = new Set(questions.map((q) => q.id));
44
+ const deps = dependencyMap(def);
25
45
  const inDegree = new Map();
26
46
  const dependents = new Map();
27
47
  for (const q of questions) {
28
- const valid = conditionRefs(q).filter((ref) => known.has(ref));
48
+ const valid = (deps.get(q.id) ?? []).filter((ref) => known.has(ref));
29
49
  inDegree.set(q.id, valid.length);
30
50
  for (const ref of valid) {
31
51
  const list = dependents.get(ref) ?? [];
@@ -49,13 +69,35 @@ export function topoOrder(def) {
49
69
  const stuck = [...known].filter((id) => !done.has(id)).sort();
50
70
  return { order, stuck };
51
71
  }
72
+ function indexCategories(def) {
73
+ const idx = {
74
+ condition: new Map(),
75
+ parent: new Map(),
76
+ categoryOf: new Map(),
77
+ ordered: [],
78
+ };
79
+ const walk = (categories, parent) => {
80
+ for (const c of categories) {
81
+ idx.condition.set(c.id, c.enable_condition);
82
+ idx.parent.set(c.id, parent);
83
+ idx.ordered.push(c.id);
84
+ for (const q of c.questions)
85
+ idx.categoryOf.set(q.id, c.id);
86
+ walk(c.children ?? [], c.id);
87
+ }
88
+ };
89
+ walk(def.categories, null);
90
+ return idx;
91
+ }
52
92
  /**
53
93
  * Đánh giá Enable Condition toàn Form theo thứ tự topo. Giá trị của Question
54
94
  * không hoạt động bị loại khỏi giá trị hiệu lực nên chuỗi phụ thuộc tắt theo.
95
+ * Question trong Category tắt cũng không hoạt động.
55
96
  */
56
97
  export function computeActive(def, values) {
57
98
  const questions = allQuestions(def);
58
99
  const byId = new Map(questions.map((q) => [q.id, q]));
100
+ const idx = indexCategories(def);
59
101
  const { order, stuck } = topoOrder(def);
60
102
  const evaluationOrder = stuck.length === 0 ? order : questions.map((q) => q.id);
61
103
  const effectiveValues = {};
@@ -64,16 +106,37 @@ export function computeActive(def, values) {
64
106
  if (v !== undefined && v !== null)
65
107
  effectiveValues[q.slug] = v;
66
108
  }
109
+ // Category hiện khi điều kiện của nó và mọi tổ tiên đều đúng. Memo an toàn
110
+ // vì thứ tự topo bảo đảm các Question được tham chiếu đã chốt giá trị
111
+ // trước khi bất kỳ Question nào bên trong Category được đánh giá.
112
+ const catMemo = new Map();
113
+ const categoryActive = (id) => {
114
+ const memoized = catMemo.get(id);
115
+ if (memoized !== undefined)
116
+ return memoized;
117
+ const parent = idx.parent.get(id) ?? null;
118
+ const cond = idx.condition.get(id);
119
+ const active = (parent === null || categoryActive(parent)) &&
120
+ (!cond || evalCondition(cond, effectiveValues, byId));
121
+ catMemo.set(id, active);
122
+ return active;
123
+ };
67
124
  const activeIds = new Set();
68
125
  for (const id of evaluationOrder) {
69
126
  const q = byId.get(id);
70
- const active = !q.enable_condition || evalCondition(q.enable_condition, effectiveValues, byId);
127
+ const cat = idx.categoryOf.get(id);
128
+ const active = (cat === undefined || categoryActive(cat)) &&
129
+ (!q.enable_condition ||
130
+ evalCondition(q.enable_condition, effectiveValues, byId));
71
131
  if (active)
72
132
  activeIds.add(id);
73
133
  else
74
134
  delete effectiveValues[q.slug];
75
135
  }
76
- return { activeIds, effectiveValues };
136
+ // Category không chứa Question nào chưa được đánh giá trong vòng trên;
137
+ // giá trị hiệu lực đã chốt nên đánh giá nốt tại đây.
138
+ const activeCategoryIds = new Set(idx.ordered.filter(categoryActive));
139
+ return { activeIds, activeCategoryIds, effectiveValues };
77
140
  }
78
141
  function evalCondition(condition, effective, byId) {
79
142
  return condition.any.some((group) => group.all.every((c) => evalSingle(c, effective, byId)));
package/dist/types.d.ts CHANGED
@@ -54,12 +54,39 @@ export interface BodyMapSpot {
54
54
  /** Metadata lâm sàng tuỳ ý; VGQ không diễn giải. */
55
55
  metadata?: JsonValue;
56
56
  }
57
+ /**
58
+ * Hướng dẫn bằng audio/video kèm lời dẫn, đặt ở Form, Category hoặc dùng làm
59
+ * audio phát khi chọn Option. Ứng dụng phát media (nếu có) và/hoặc đọc `text`.
60
+ */
61
+ export interface Guidance {
62
+ /** Media Asset dạng audio hoặc video; vắng mặt nghĩa là chỉ có lời dẫn. */
63
+ media_asset_id?: string;
64
+ text?: string;
65
+ }
66
+ /** Thang đau VAS: các khoảng điểm liên tiếp kèm mô tả để tự đối chiếu. */
67
+ export interface PainScale {
68
+ enabled: boolean;
69
+ ranges: PainRange[];
70
+ }
71
+ export interface PainRange {
72
+ start: number;
73
+ end: number;
74
+ label?: string;
75
+ }
57
76
  export interface OptionDef {
58
77
  id: string;
59
78
  slug: string;
60
79
  label: string;
61
80
  /** Chỉ có mặt khi Question có data type body_map. */
62
81
  body_map?: BodyMapSpot | null;
82
+ /**
83
+ * Option cha trong cùng Question, theo id nội bộ. Body Map nhiều tầng: vùng
84
+ * lớn là Option cha, điểm chi tiết là Option con. Response Value vẫn phẳng.
85
+ */
86
+ parent_id?: string | null;
87
+ /** Audio phát khi Respondent chọn Option. */
88
+ audio?: Guidance | null;
89
+ pain_scale?: PainScale | null;
63
90
  }
64
91
  export interface MediaAssetDto {
65
92
  id: string;
@@ -88,11 +115,21 @@ export interface CategoryDef {
88
115
  label: string;
89
116
  children?: CategoryDef[];
90
117
  questions: QuestionDef[];
118
+ /** Hướng dẫn mở đầu cho riêng Category này. */
119
+ guidance?: Guidance | null;
120
+ /**
121
+ * Điều kiện hiện cả Category (ví dụ tab bệnh hiện theo lý do đến khám).
122
+ * Category tắt thì mọi Question bên trong — kể cả trong Category con —
123
+ * tắt theo và giá trị của chúng bị loại khỏi official values.
124
+ */
125
+ enable_condition?: EnableCondition | null;
91
126
  }
92
127
  export interface FormDefinition {
93
128
  title: string;
94
129
  form_type: FormType;
95
130
  categories: CategoryDef[];
131
+ /** Hướng dẫn mở đầu cho cả Form. */
132
+ guidance?: Guidance | null;
96
133
  }
97
134
  export interface ValidationIssue {
98
135
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viengut/vgq-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Headless TypeScript SDK for the VGQ clinical form platform: load published Forms, manage Response state, evaluate Enable Conditions and validate Response Values without any UI components.",
5
5
  "repository": {
6
6
  "type": "git",