@aigne/doc-smith 0.2.11 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -495,6 +495,28 @@ flowchart TD
495
495
  OperationalExcellence --> "Maintainability & Scalability" --> L;
496
496
  \`\`\`
497
497
 
498
+ This content ends properly.
499
+ `,
500
+ },
501
+
502
+ {
503
+ category: "🧩 MERMAID VALIDATION",
504
+ name: "Mermaid with numbered list format in node labels",
505
+ expectPass: false,
506
+ expectedErrors: ["numbered list format in Mermaid node label"],
507
+ content: `# Test Document
508
+
509
+ \`\`\`mermaid
510
+ flowchart TD
511
+ A["1. Create Backend Implementation<br>api/src/providers/"]
512
+ B["2. Add Backend Configuration<br>api/src/providers/models.ts"]
513
+ C["3. Update Frontend Selector<br>src/pages/config/ai-providers/"]
514
+ D["4. Add Provider Logo<br>public/logo/"]
515
+ E["5. Update Documentation"]
516
+
517
+ A --> B --> C --> D --> E
518
+ \`\`\`
519
+
498
520
  This content ends properly.
499
521
  `,
500
522
  },
@@ -3,11 +3,20 @@ import { readFile, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { createConnect } from "@aigne/aigne-hub";
6
+ import chalk from "chalk";
6
7
  import open from "open";
7
8
  import { joinURL } from "ufo";
8
9
  import { parse, stringify } from "yaml";
9
- import { getComponentMountPoint } from "./blocklet.mjs";
10
- import { DISCUSS_KIT_DID } from "./constants.mjs";
10
+ import {
11
+ getComponentMountPoint,
12
+ InvalidBlockletError,
13
+ ComponentNotFoundError,
14
+ } from "./blocklet.mjs";
15
+ import {
16
+ DISCUSS_KIT_DID,
17
+ DISCUSS_KIT_STORE_URL,
18
+ BLOCKLET_ADD_COMPONENT_DOCS,
19
+ } from "./constants.mjs";
11
20
 
12
21
  const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
13
22
 
@@ -47,14 +56,29 @@ export async function getAccessToken(appUrl) {
47
56
  // Check if Discuss Kit is running at the provided URL
48
57
  try {
49
58
  await getComponentMountPoint(appUrl, DISCUSS_KIT_DID);
50
- } catch {
51
- throw new Error(
52
- `Unable to find Discuss Kit running at the provided URL: ${appUrl}\n\n` +
53
- "Please ensure that:\n" +
54
- "The URL is correct and accessible\n" +
55
- " Discuss Kit is properly installed and running\n" +
56
- "If you continue to experience issues, please verify your Discuss Kit installation.",
57
- );
59
+ } catch (error) {
60
+ const storeLink = chalk.cyan(DISCUSS_KIT_STORE_URL);
61
+ if (error instanceof InvalidBlockletError) {
62
+ throw new Error(
63
+ `${chalk.yellow("⚠️ The provided URL is not a valid website on ArcBlock platform")}\n\n` +
64
+ `${chalk.bold("💡 Solution:")} Start here to run your own website that can host your docs:\n${storeLink}\n\n`,
65
+ );
66
+ } else if (error instanceof ComponentNotFoundError) {
67
+ const docsLink = chalk.cyan(BLOCKLET_ADD_COMPONENT_DOCS);
68
+ throw new Error(
69
+ `${chalk.yellow("⚠️ This website does not have required components for publishing")}\n\n` +
70
+ `${chalk.bold("💡 Solution:")} Please refer to the documentation to add Discuss Kit component:\n${docsLink}\n\n`,
71
+ );
72
+ } else {
73
+ throw new Error(
74
+ `❌ Unable to connect to: ${chalk.cyan(appUrl)}\n\n` +
75
+ `${chalk.bold("Possible causes:")}\n` +
76
+ `• Network connection issues\n` +
77
+ `• Server temporarily unavailable\n` +
78
+ `• Incorrect URL address\n\n` +
79
+ `${chalk.green("Suggestion:")} Please check your network connection and URL address, then try again`,
80
+ );
81
+ }
58
82
  }
59
83
 
60
84
  const DISCUSS_KIT_URL = appUrl;
@@ -1,24 +1,58 @@
1
+ /**
2
+ * Custom error class for invalid blocklet application URLs
3
+ */
4
+ export class InvalidBlockletError extends Error {
5
+ constructor(url, status, statusText) {
6
+ super(`Invalid application URL: "${url}". Unable to fetch configuration.`);
7
+ this.name = "InvalidBlockletError";
8
+ this.url = url;
9
+ this.status = status;
10
+ this.statusText = statusText;
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Custom error class for missing component mount points
16
+ */
17
+ export class ComponentNotFoundError extends Error {
18
+ constructor(did, appUrl) {
19
+ super(`Your website "${appUrl}" missing required component to host your docs.`);
20
+ this.name = "ComponentNotFoundError";
21
+ this.did = did;
22
+ this.appUrl = appUrl;
23
+ }
24
+ }
25
+
1
26
  export async function getComponentMountPoint(appUrl, did) {
2
27
  const url = new URL(appUrl);
3
28
  const blockletJsUrl = `${url.origin}/__blocklet__.js?type=json`;
4
29
 
5
- const blockletJs = await fetch(blockletJsUrl, {
6
- method: "GET",
7
- headers: {
8
- Accept: "application/json",
9
- },
10
- });
30
+ let blockletJs;
31
+ try {
32
+ blockletJs = await fetch(blockletJsUrl, {
33
+ method: "GET",
34
+ headers: {
35
+ Accept: "application/json",
36
+ },
37
+ });
38
+ } catch (error) {
39
+ throw new InvalidBlockletError(appUrl, null, error.message);
40
+ }
11
41
 
12
42
  if (!blockletJs.ok) {
13
- throw new Error(
14
- `Failed to fetch blocklet json: ${blockletJs.status} ${blockletJs.statusText}, ${blockletJsUrl}`,
15
- );
43
+ throw new InvalidBlockletError(appUrl, blockletJs.status, blockletJs.statusText);
44
+ }
45
+
46
+ let config;
47
+ try {
48
+ config = await blockletJs.json();
49
+ } catch {
50
+ throw new InvalidBlockletError(appUrl, null, "Invalid JSON response");
16
51
  }
17
52
 
18
- const config = await blockletJs.json();
19
- const component = config.componentMountPoints.find((component) => component.did === did);
53
+ const component = config.componentMountPoints?.find((component) => component.did === did);
20
54
  if (!component) {
21
- throw new Error(`Component ${did} not found in blocklet: ${appUrl}`);
55
+ throw new ComponentNotFoundError(did, appUrl);
22
56
  }
23
57
 
24
58
  return component.mountPoint;
@@ -0,0 +1,131 @@
1
+ import { CONFLICT_RULES } from "./constants.mjs";
2
+
3
+ /**
4
+ * Detect internal conflicts within the same question (multi-select conflicts)
5
+ * @param {string} questionType - Question type (documentPurpose, targetAudienceTypes)
6
+ * @param {Array} selectedValues - User selected values
7
+ * @returns {Array} List of conflicts
8
+ */
9
+ export function detectInternalConflicts(questionType, selectedValues) {
10
+ const rules = CONFLICT_RULES.internalConflicts[questionType] || [];
11
+
12
+ // Extract values from the selected items (handle both string arrays and object arrays)
13
+ const selectedValueStrings = selectedValues.map((item) =>
14
+ typeof item === "object" && item.value ? item.value : item,
15
+ );
16
+
17
+ const conflicts = [];
18
+
19
+ rules.forEach((rule) => {
20
+ // Check if all conflict items are selected
21
+ const hasConflict = rule.conflictItems.every((item) => selectedValueStrings.includes(item));
22
+
23
+ if (hasConflict) {
24
+ conflicts.push({
25
+ type: "internal",
26
+ questionType,
27
+ severity: rule.severity,
28
+ reason: rule.reason,
29
+ suggestion: rule.suggestion,
30
+ items: rule.conflictItems,
31
+ });
32
+ }
33
+ });
34
+
35
+ return conflicts;
36
+ }
37
+
38
+ /**
39
+ * Get filtered options based on cross-question conflict rules
40
+ * @param {string} targetQuestion - Target question type to filter
41
+ * @param {Object} currentSelections - Current user selections across all questions
42
+ * @param {Object} allOptions - All available options for the target question
43
+ * @returns {Object} Filtered options
44
+ */
45
+ export function getFilteredOptions(targetQuestion, currentSelections, allOptions) {
46
+ const crossRules = CONFLICT_RULES.crossConflicts;
47
+ const filteredOptions = { ...allOptions };
48
+ const appliedFilters = [];
49
+
50
+ crossRules.forEach((rule) => {
51
+ // Check if we should apply this rule for filtering the target question
52
+ const shouldApplyRule = (() => {
53
+ // Check if this rule applies to the target question
54
+ const conflictingForTarget = rule.conflictingOptions[targetQuestion];
55
+ if (!conflictingForTarget) {
56
+ return false;
57
+ }
58
+
59
+ // Check if current selections match the conditions that would trigger filtering
60
+ const nonTargetConditions = Object.entries(rule.conditions).filter(
61
+ ([key]) => key !== targetQuestion,
62
+ );
63
+
64
+ if (nonTargetConditions.length === 0) {
65
+ return false;
66
+ }
67
+
68
+ const matchesNonTargetConditions = nonTargetConditions.every(([key, values]) => {
69
+ const selection = currentSelections[key];
70
+ if (!selection) return false;
71
+
72
+ // Extract values if selection contains objects
73
+ const selectionArray = Array.isArray(selection) ? selection : [selection];
74
+ const selectionValues = selectionArray.map((item) =>
75
+ typeof item === "object" && item.value ? item.value : item,
76
+ );
77
+
78
+ return values.some((value) => selectionValues.includes(value));
79
+ });
80
+
81
+ return matchesNonTargetConditions;
82
+ })();
83
+
84
+ if (shouldApplyRule && rule.action === "filter") {
85
+ // Filter out conflicting options for the target question
86
+ const conflictingForTarget = rule.conflictingOptions[targetQuestion];
87
+
88
+ if (conflictingForTarget) {
89
+ conflictingForTarget.forEach((conflictOption) => {
90
+ if (filteredOptions[conflictOption]) {
91
+ delete filteredOptions[conflictOption];
92
+ appliedFilters.push({
93
+ removedOption: conflictOption,
94
+ reason: rule.reason,
95
+ severity: rule.severity,
96
+ });
97
+ }
98
+ });
99
+ }
100
+ }
101
+ });
102
+
103
+ return {
104
+ filteredOptions,
105
+ appliedFilters,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Validate user selection for internal conflicts and return validation message
111
+ * @param {string} questionType - Question type (documentPurpose, targetAudienceTypes)
112
+ * @param {Array} selectedValues - User selected values
113
+ * @returns {string|boolean} Error message if conflicts exist, true if valid
114
+ */
115
+ export function validateSelection(questionType, selectedValues) {
116
+ const conflicts = detectInternalConflicts(questionType, selectedValues);
117
+
118
+ if (conflicts.length === 0) {
119
+ return true;
120
+ }
121
+
122
+ // Return error message for severe conflicts
123
+ const severeConflicts = conflicts.filter((c) => c.severity === "severe");
124
+ if (severeConflicts.length > 0) {
125
+ const conflict = severeConflicts[0];
126
+ return `Conflict detected: ${conflict.reason}. ${conflict.suggestion}`;
127
+ }
128
+
129
+ // For moderate conflicts, allow but warn
130
+ return true;
131
+ }