@amityco/social-plus-vise 1.2.0 → 1.3.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.
@@ -764,11 +764,15 @@ export async function generateDesignReference(repoPath, contract, title) {
764
764
  .join("\n ");
765
765
  const digestShort = esc(contract.digest.slice(0, 23));
766
766
  const logoLetter = esc(title.slice(0, 1).toUpperCase());
767
+ const rootDecls = [...tokenCss.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)]
768
+ .map((m) => ` ${m[1]}: ${safeCss(m[2].trim())};`)
769
+ .join("\n");
770
+ const rootCss = rootDecls ? `:root{\n${rootDecls}\n}` : "";
767
771
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"/>
768
772
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
769
773
  <title>${esc(title)} — Design System</title>
770
774
  <style>
771
- ${tokenCss}
775
+ ${rootCss}
772
776
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
773
777
  html{scroll-behavior:smooth}
774
778
  body{
@@ -1094,6 +1098,8 @@ export async function runDesignCheck(repoPath) {
1094
1098
  const declaredTokens = contract.tokens.filter((token) => token.provenance === "declared");
1095
1099
  const contractColorValues = new Set(contract.tokens.filter((token) => token.category === "color").map((token) => token.value));
1096
1100
  const referenced = new Set();
1101
+ const referencedInApp = new Set();
1102
+ const seedTokensRel = SP_TOKENS_PATH.split("/").join(path.sep);
1097
1103
  const colorSample = [];
1098
1104
  const definedVars = new Set();
1099
1105
  const varRefs = [];
@@ -1115,13 +1121,18 @@ export async function runDesignCheck(repoPath) {
1115
1121
  scanned += 1;
1116
1122
  const rel = path.relative(repoRoot, file);
1117
1123
  const isCss = file.toLowerCase().endsWith(".css") || file.toLowerCase().endsWith(".scss");
1124
+ const isSeedFile = rel === SP_TOKENS_PATH || rel === seedTokensRel;
1125
+ const contentLower = content.toLowerCase();
1118
1126
  for (const token of declaredTokens) {
1119
1127
  const key = tokenKey(token);
1120
- if (referenced.has(key)) {
1128
+ if (referenced.has(key) && referencedInApp.has(key)) {
1121
1129
  continue;
1122
1130
  }
1123
- if ((token.name && content.includes(token.name)) || content.includes(token.value)) {
1131
+ if ((token.name && content.includes(token.name)) || contentLower.includes(token.value.toLowerCase())) {
1124
1132
  referenced.add(key);
1133
+ if (!isSeedFile) {
1134
+ referencedInApp.add(key);
1135
+ }
1125
1136
  }
1126
1137
  }
1127
1138
  for (const value of scanColorLiterals(content)) {
@@ -1144,12 +1155,19 @@ export async function runDesignCheck(repoPath) {
1144
1155
  }
1145
1156
  const referencedTokens = declaredTokens.filter((token) => referenced.has(tokenKey(token))).map((token) => token.name ?? token.value);
1146
1157
  const unreferencedTokens = declaredTokens.filter((token) => !referenced.has(tokenKey(token))).map((token) => token.name ?? token.value);
1158
+ const seededOnlyTokens = declaredTokens
1159
+ .filter((token) => referenced.has(tokenKey(token)) && !referencedInApp.has(tokenKey(token)))
1160
+ .map((token) => token.name ?? token.value);
1147
1161
  const contractTokenNames = new Set(contract.tokens.map((token) => token.name).filter((name) => Boolean(name)));
1148
1162
  const undefinedRefs = dedupeByToken(varRefs.filter((ref) => !definedVars.has(ref.token) && !contractTokenNames.has(ref.token)));
1149
1163
  return {
1150
1164
  status: "advisory",
1151
1165
  message: `Checked ${scanned} source file(s) against design contract ${contract.digest}. ` +
1152
- `${referencedTokens.length}/${declaredTokens.length} declared tokens referenced; ${totalColors - onContract} of ${totalColors} color literal(s) are off-contract (review hints)` +
1166
+ `${referencedTokens.length}/${declaredTokens.length} declared tokens referenced` +
1167
+ (seededOnlyTokens.length > 0
1168
+ ? ` (${referencedTokens.length - seededOnlyTokens.length} used in your code, ${seededOnlyTokens.length} only in the Vise-seeded ${SP_TOKENS_PATH} — apply them to count)`
1169
+ : "") +
1170
+ `; ${totalColors - onContract} of ${totalColors} color literal(s) are off-contract (review hints)` +
1153
1171
  (undefinedRefs.length > 0 ? `; ${undefinedRefs.length} undefined token reference(s) (likely broken styles)` : "") +
1154
1172
  ".",
1155
1173
  contract: contractSummary(contract),
@@ -1158,6 +1176,8 @@ export async function runDesignCheck(repoPath) {
1158
1176
  referenced: referencedTokens.length,
1159
1177
  referenced_tokens: referencedTokens,
1160
1178
  unreferenced_tokens: unreferencedTokens,
1179
+ referenced_in_app: referencedTokens.length - seededOnlyTokens.length,
1180
+ seeded_only_tokens: seededOnlyTokens,
1161
1181
  },
1162
1182
  colorLiterals: {
1163
1183
  scanned_files: scanned,
@@ -220,7 +220,7 @@ async function validateAndroid(root) {
220
220
  findings.push(finding("android.dependency.sdk", "warning", "No obvious social.plus Android SDK dependency was found in Gradle files.", relativeFile(root, buildFiles[0]), "Add the SDK dependency from the Android quick-start docs and keep all Amity dependencies on compatible versions."));
221
221
  }
222
222
  else if (containsAny(buildContent, [/co\.amity\.android:amity-sdk:\+/, /co\.amity\.android:amity-sdk:latest/i, /version\s*=\s*["']\+["']/])) {
223
- findings.push(finding("android.sdk.version.pinned", "warning", "The Android SDK dependency appears to use a floating version.", relativeFile(root, buildFiles[0]), "Pin the social.plus Android SDK to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
223
+ findings.push(finding("android.sdk.version.pinned", "warning", "The Android SDK dependency uses an uncontrolled version (e.g. latest / + / a moving branch), not a pinned version or reviewed semver range.", relativeFile(root, buildFiles[0]), "Pin the social.plus Android SDK to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
224
224
  }
225
225
  if (setupFiles.length === 0) {
226
226
  findings.push(finding("android.setup.present", "warning", "No obvious AmityCoreClient.setup call was found in Kotlin/Java files.", undefined, "Call SDK setup once during application startup before any social.plus API usage."));
@@ -327,7 +327,7 @@ async function validateFlutter(root) {
327
327
  findings.push(finding("flutter.dependency.sdk", "warning", "No obvious social.plus/Amity dependency was found in pubspec.yaml.", "pubspec.yaml", "Add the Flutter SDK dependency from the Flutter quick-start docs."));
328
328
  }
329
329
  else if (/\bamity_sdk\s*:\s*(?:any|\*|latest)\b/i.test(pubspec)) {
330
- findings.push(finding("flutter.sdk.version.pinned", "warning", "The Flutter SDK dependency appears to use a floating version.", "pubspec.yaml", "Pin amity_sdk to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
330
+ findings.push(finding("flutter.sdk.version.pinned", "warning", "The Flutter SDK dependency uses an uncontrolled version (e.g. any / a moving git ref), not a pinned version or reviewed semver range.", "pubspec.yaml", "Pin amity_sdk to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
331
331
  }
332
332
  if (setupFiles.length === 0) {
333
333
  findings.push(finding("flutter.setup.present", "warning", "No obvious AmityCoreClient.setup call was found in lib/*.dart.", undefined, "Call AmityCoreClient.setup before any social.plus API usage."));
@@ -424,7 +424,7 @@ async function validateTypeScript(root, platform) {
424
424
  findings.push(finding("typescript.dependency.sdk", "warning", "No obvious social.plus/Amity package was found in package.json.", "package.json", "Add the TypeScript SDK dependency from the web quick-start docs."));
425
425
  }
426
426
  else if (isFloatingPackageDependency(packageJson, "@amityco/ts-sdk")) {
427
- findings.push(finding(`${platform}.sdk.version.pinned`, "warning", "The TypeScript SDK dependency appears to use a floating version.", "package.json", "Pin @amityco/ts-sdk to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
427
+ findings.push(finding(`${platform}.sdk.version.pinned`, "warning", "The TypeScript SDK dependency uses an uncontrolled version (latest / * / x), not a pinned version or reviewed semver range (^/~ is accepted).", "package.json", "Pin @amityco/ts-sdk to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
428
428
  }
429
429
  if (setupFiles.length === 0) {
430
430
  findings.push(finding("typescript.client.create", "warning", "No obvious TypeScript client initialization pattern was found.", undefined, "Create the social.plus client before login and before API usage."));
@@ -940,13 +940,18 @@ function validateComments(root, platform, sourceContent) {
940
940
  break;
941
941
  }
942
942
  }
943
- const hasReactiveCommentObserver = [...sourceContent.values()].some((c) => {
943
+ const isObserverContent = (c) => {
944
944
  const stripped = c.replace(/\bawait\b[^\n;]*?(?:get|query)Comments\s*\(/g, "");
945
945
  return /(?:get|query)Comments\s*\(/.test(stripped)
946
946
  || /CommentLiveCollection|AmityCommentCollection|commentCollection/.test(c);
947
- });
947
+ };
948
+ const observerFiles = [...sourceContent.entries()].filter(([, c]) => isObserverContent(c)).map(([f]) => f);
949
+ const hasReactiveCommentObserver = observerFiles.length > 0;
948
950
  const cleanupMarkers = COMMENT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM[platform] ?? COMMENT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM.typescript;
949
- if (commentFiles.length > 0 && hasReactiveCommentObserver && !containsAny(sourceContent, cleanupMarkers)) {
951
+ const ANDROID_OBSERVER_SELF_CLEAN = /(?:(?:get|query)Comments\s*\(|CommentLiveCollection|AmityCommentCollection|commentCollection)[^;{}]{0,300}?(?:\.stateIn\s*\([^;{}]*\bviewModelScope\b|\.asLiveData\s*\(|\.collectAsStateWithLifecycle\s*\(|\brepeatOnLifecycle\b|\bDisposableEffect\b)/;
952
+ const observerSelfCleans = platform === "android" &&
953
+ observerFiles.some((file) => ANDROID_OBSERVER_SELF_CLEAN.test(commentStripped(file, platform, sourceContent.get(file) ?? "")));
954
+ if (commentFiles.length > 0 && hasReactiveCommentObserver && !observerSelfCleans && !containsAny(sourceContent, cleanupMarkers)) {
950
955
  findings.push(finding(`${platform}.comments.observer-cleanup`, "warning", "Comment observer/subscription was found but no obvious cleanup was detected.", relativeFile(root, commentFiles[0]), "Dispose or unsubscribe comment observers when the lifecycle owner is destroyed."));
951
956
  }
952
957
  const strippedCommentFiles = new Map();
@@ -1032,6 +1037,16 @@ const CHAT_PRESENCE_PATTERNS = [
1032
1037
  /\bchatClient\b/i,
1033
1038
  /\bmessage\s*live\s*collection\b/i,
1034
1039
  ];
1040
+ const CHAT_SEND_CALL_PATTERNS = [
1041
+ /\bsendMessage\s*\(/,
1042
+ /\bcreateMessage\b/,
1043
+ /\bcreateTextMessage\b/,
1044
+ /\bcreateImageMessage\b/,
1045
+ /\bcreateFileMessage\b/,
1046
+ /\bcreateVideoMessage\b/,
1047
+ /\bcreateAudioMessage\b/,
1048
+ /\bcreateCustomMessage\b/,
1049
+ ];
1035
1050
  const CHAT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM = {
1036
1051
  typescript: [/\bunsubscribe\b/, /\bdispose\b/, /\bremoveListener\b/, /\buseEffect\b.*\breturn\b/s],
1037
1052
  "react-native": [/\bunsubscribe\b/, /\bdispose\b/, /\bremoveListener\b/, /\buseEffect\b.*\breturn\b/s],
@@ -1071,6 +1086,7 @@ function validateChat(root, platform, sourceContent) {
1071
1086
  const chatFileContent = new Map();
1072
1087
  for (const f of chatFiles)
1073
1088
  chatFileContent.set(f, sourceContent.get(f) ?? "");
1089
+ const hasSendCall = containsAny(chatFileContent, CHAT_SEND_CALL_PATTERNS);
1074
1090
  const hasErrorHandling = containsAny(chatFileContent, [
1075
1091
  /\bcatch\b/,
1076
1092
  /\b\.catch\b/,
@@ -1080,7 +1096,7 @@ function validateChat(root, platform, sourceContent) {
1080
1096
  /\berror\s*:/,
1081
1097
  /\btry\b/,
1082
1098
  ]);
1083
- if (!hasErrorHandling) {
1099
+ if (hasSendCall && !hasErrorHandling) {
1084
1100
  findings.push(finding(`${platform}.chat.send-error-handling`, "warning", "Message send calls found without error handling.", relativeFile(root, chatFiles[0]), "Wrap sendMessage in try/catch or handle the error callback to show send failure state."));
1085
1101
  }
1086
1102
  if (!containsAny(chatFileContent, MODERATION_MARKERS)) {
@@ -1450,6 +1466,8 @@ function validatePostsStatusFilter(root, platform, sourceContent) {
1450
1466
  /\/\/\s*vise:\s*moderation review feed/i
1451
1467
  ];
1452
1468
  for (const [file, content] of sourceContent) {
1469
+ if (path.basename(file).endsWith('.d.ts'))
1470
+ continue;
1453
1471
  const rel = relativeFile(root, file);
1454
1472
  const hasPostQuery = POST_QUERY_PATTERNS.some((p) => p.test(content));
1455
1473
  if (hasPostQuery) {
@@ -2305,7 +2323,7 @@ async function validateIos(root) {
2305
2323
  findings.push(finding("ios.dependency.swiftpm-repo", "warning", "The iOS SwiftPM dependency points at the obsolete Amity-Social-Cloud-SDK-iOS-IPA package repository.", relativeFile(root, manifestFiles[0]), "Use https://github.com/AmityCo/Amity-Social-Cloud-SDK-iOS-SwiftPM.git with product AmitySDK so SwiftPM can resolve the social.plus iOS SDK."));
2306
2324
  }
2307
2325
  else if (containsAny(manifestContent, [/\.package\s*\([^)]*\bbranch\s*:/s, /pod\s+['"][^'"]*Amity[^'"]*['"]\s*$/m])) {
2308
- findings.push(finding("ios.sdk.version.pinned", "warning", "The iOS SDK dependency appears to use a floating branch or unspecified CocoaPods version.", relativeFile(root, manifestFiles[0]), "Pin the social.plus iOS SDK to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
2326
+ findings.push(finding("ios.sdk.version.pinned", "warning", "The iOS SDK dependency uses an uncontrolled version (a moving branch or unspecified CocoaPods version), not a pinned version or reviewed semver range.", relativeFile(root, manifestFiles[0]), "Pin the social.plus iOS SDK to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
2309
2327
  }
2310
2328
  if (setupFiles.length === 0) {
2311
2329
  findings.push(finding("ios.setup.present", "warning", "No obvious AmityClient initialization was found in Swift files.", undefined, "Initialize AmityClient before login and before social.plus API usage."));
@@ -174,9 +174,16 @@ export async function getSdkFacts(options) {
174
174
  notes: [
175
175
  "Symbol facts are existence-only: Vise confirms public symbols in the normalized SDK surface, but semantic correctness and idiomatic usage still require rules, docs, and sensors.",
176
176
  modelFacts.facts
177
- ? `Field-level model facts are extraction-grounded (${modelFacts.facts.fieldsGrounding}) by ${modelFacts.facts.extraction.extractor}; names-only grounding means the source proves field names but no types.`
177
+ ? `Field-level model facts are extraction-grounded (${modelFacts.facts.fieldsGrounding}) by ${modelFacts.facts.extraction.extractor}.${modelFacts.facts.fieldsGrounding === "names-only"
178
+ ? " names-only grounding means the source proves field names but no types."
179
+ : ""}`
178
180
  : "No field-level model facts for this platform snapshot: model claims stay symbol-only (absence over fabrication).",
179
181
  "Remote SP_SDK_SURFACE_URL resolution is recorded in the manifest for a future online snapshot source; this MVP uses local override/env/bundled directories only.",
182
+ ...(platform !== "typescript"
183
+ ? [
184
+ `Capability anchors are authored TypeScript-first at this MVP, so capabilities is empty for ${platform}. This means capability mapping is not yet available on ${platform} — it does NOT mean the SDK lacks the feature. The symbol and model facts above are extracted from the ${platform} surface and remain authoritative.`,
185
+ ]
186
+ : []),
180
187
  ],
181
188
  };
182
189
  if (options.includeSymbols) {
@@ -4,7 +4,7 @@ const SIGNALS = [
4
4
  level: "dynamic-ui",
5
5
  label: "Minimal, server-driven brand or theme updates",
6
6
  strength: "strong",
7
- pattern: /\b(dynamic ui|remote config|network config|syncNetworkConfig|without (?:an? )?(?:app )?(?:release|update|rebuild)|real[-\s]?time theme|brand colors?|theme tokens?|palette|console[-\s]?driven)\b/i,
7
+ pattern: /\b(dynamic ui|remote config|network config|syncNetworkConfig|without (?:an? )?(?:app )?(?:release|update|rebuild)|(?:with no|no)\s+(?:an? )?(?:app )?(?:release|rebuild|re-?deploy(?:ment)?|deploy)|real[-\s]?time theme|brand colors?|theme tokens?|palette|console[-\s]?driven)\b/i,
8
8
  },
9
9
  {
10
10
  id: "component-styling",
@@ -39,7 +39,7 @@ const SIGNALS = [
39
39
  level: "sdk-custom-ui",
40
40
  label: "Custom UI beyond UIKit's customization model",
41
41
  strength: "strong",
42
- pattern: /\b(totally different ui|brand[-\s]?new ui|bespoke ui|unique ui|complete design freedom|build from scratch|hand[-\s]?rolled)\b/i,
42
+ pattern: /\b(totally different ui|brand[-\s]?new ui|bespoke ui|unique ui|complete design freedom|build from scratch|hand[-\s]?rolled)\b|\b(?:our|my|their|its|your)\s+own\s+(?:\w+\s+){0,2}?(?:component(?:\s+tree)?|components?|screens?|widgets?|ui)\b/i,
43
43
  },
44
44
  {
45
45
  id: "basic-theme",
@@ -71,6 +71,10 @@ const SOURCE_EVIDENCE = [
71
71
  "Local UIKit source study: Web provider exposes configs, pageBehavior, syncNetworkConfig, and localization props; React Native, Android, and Flutter repos use config files plus behavior managers/providers.",
72
72
  ];
73
73
  const FEATURE_CAPABILITY = /\b(livestream(?:ing)?|live[-\s]?stream(?:ing)?|video call(?:ing|s)?|voice call(?:ing|s)?|video chat|broadcast(?:ing)?|go live)\b/i;
74
+ const SERVER_DRIVEN_ORIGIN = /\b(remote config|network config|syncNetworkConfig|real[-\s]?time theme|server[-\s]?driven|(?:without|with no|no)\s+(?:an? )?(?:app )?(?:release|rebuild|re-?deploy(?:ment)?|deploy)|update(?:d|s)? remotely|remotely (?:update|change|configure|manage)\w*)\b/i;
75
+ const BUNDLED_CONFIG_ORIGIN = /\b(config\.json|uikit\.config|bundled config|config file)\b/i;
76
+ const THEME_TOKEN_STYLING = /\b(primary_color|preferred_theme|dark mode|light mode|brand colors?|palette|theme)\b/i;
77
+ const STRUCTURAL_STYLING = /\b((?:hide|remove)\b|exclu(?:de|des|ded|ding|sion|sions)|button text|icons?|page id|component id|element id|reactions?|feature flags?)\b/i;
74
78
  export function recommendUIKitCustomization(args) {
75
79
  const answers = args.answers ?? {};
76
80
  const request = typeof args.request === "string" ? args.request : "";
@@ -94,7 +98,7 @@ export function recommendUIKitCustomization(args) {
94
98
  const matchedLevels = levelsFromMatches(matches);
95
99
  const answeredLevel = explicitAnswer;
96
100
  const recommendedLevel = answeredLevel
97
- ?? (unrecognizedExplicitAnswer || featureCapabilityOnly ? "needs-decision" : chooseRecommendedLevel(matches, args.solutionPath));
101
+ ?? (unrecognizedExplicitAnswer || featureCapabilityOnly ? "needs-decision" : chooseRecommendedLevel(matches, request, args.solutionPath));
98
102
  const additionalLevels = matchedLevels.filter((level) => level !== recommendedLevel);
99
103
  const status = statusFor(recommendedLevel, matchedLevels, args.solutionPath, explicitAnswer, unrecognizedExplicitAnswer);
100
104
  const confidence = confidenceFor({ explicitAnswer, matches, recommendedLevel, status, solutionPath: args.solutionPath, unrecognizedExplicitAnswer });
@@ -127,9 +131,11 @@ export function recommendUIKitCustomization(args) {
127
131
  advisoryOnly: "This is advisory UIKit customization guidance. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
128
132
  };
129
133
  }
134
+ const NEGATED_CUSTOM_CODE = /\b(?:do not|don'?t|dont|won'?t|wont|can'?t|cant|cannot|no|without|not|never|avoid|skip)\b(?:\s+\w+){0,3}?\s+(?:run\s+)?custom\s+(?:code|handler|action)\b/gi;
130
135
  function collectSignals(request) {
136
+ const cleaned = request.replace(NEGATED_CUSTOM_CODE, " ");
131
137
  return SIGNALS.flatMap((signal) => {
132
- const match = request.match(signal.pattern);
138
+ const match = cleaned.match(signal.pattern);
133
139
  if (!match?.[0]) {
134
140
  return [];
135
141
  }
@@ -162,7 +168,7 @@ function levelsFromMatches(matches) {
162
168
  "sdk-custom-ui",
163
169
  ].filter((level) => matches.some((match) => match.level === level));
164
170
  }
165
- function chooseRecommendedLevel(matches, solutionPath) {
171
+ function chooseRecommendedLevel(matches, request, solutionPath) {
166
172
  const matchedLevels = levelsFromMatches(matches);
167
173
  const strongLevels = levelsFromMatches(matches.filter((match) => match.strength === "strong"));
168
174
  const pool = strongLevels.length > 0 ? strongLevels : matchedLevels;
@@ -175,6 +181,14 @@ function chooseRecommendedLevel(matches, solutionPath) {
175
181
  if (pool.includes("behavior-overrides")) {
176
182
  return "behavior-overrides";
177
183
  }
184
+ if (pool.includes("dynamic-ui") &&
185
+ pool.includes("component-styling") &&
186
+ SERVER_DRIVEN_ORIGIN.test(request) &&
187
+ !BUNDLED_CONFIG_ORIGIN.test(request) &&
188
+ THEME_TOKEN_STYLING.test(request) &&
189
+ !STRUCTURAL_STYLING.test(request)) {
190
+ return "dynamic-ui";
191
+ }
178
192
  if (pool.includes("component-styling")) {
179
193
  return "component-styling";
180
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",