@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.
- package/CHANGELOG.md +30 -0
- package/README.md +4 -1
- package/dist/capabilities.js +6 -3
- package/dist/explore.js +51 -0
- package/dist/humanFormat.js +226 -0
- package/dist/outcomes.js +15 -14
- package/dist/server.js +110 -38
- package/dist/solutionPath.js +1 -1
- package/dist/tools/compliance.js +289 -35
- package/dist/tools/debug.js +83 -26
- package/dist/tools/design.js +24 -4
- package/dist/tools/project.js +26 -8
- package/dist/tools/sdkFacts.js +8 -1
- package/dist/uikitCustomization.js +19 -5
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
|
|
9
|
+
import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, recordBaseline, initEngagement, initEngagementTool, listRules, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
|
|
10
10
|
import { designCheckTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
|
|
11
11
|
import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
|
|
12
12
|
import { compileExperienceTool } from "./tools/experienceCompiler.js";
|
|
@@ -22,7 +22,10 @@ import { addBlockInstall, listRegistryBlocks, planBlockInstall, validateBlockIns
|
|
|
22
22
|
import { debugIssueTool, debugIssue } from "./tools/debug.js";
|
|
23
23
|
import { creativeAcceptTool, creativeBriefTool } from "./tools/creative.js";
|
|
24
24
|
import { uxHarnessTool } from "./tools/uxHarness.js";
|
|
25
|
+
import { exploreRequest } from "./explore.js";
|
|
26
|
+
import { renderHuman, wantsHumanFormat } from "./humanFormat.js";
|
|
25
27
|
import { packageName, packageVersion } from "./version.js";
|
|
28
|
+
const SUPPORT_URL = "mailto:support@social.plus";
|
|
26
29
|
const tools = new Map([
|
|
27
30
|
searchDocsTool,
|
|
28
31
|
getDocPageTool,
|
|
@@ -107,14 +110,14 @@ async function handleCli(args) {
|
|
|
107
110
|
console.log(helpText(args[1]));
|
|
108
111
|
return "exit";
|
|
109
112
|
}
|
|
110
|
-
if (command === "doctor" || command === "--doctor") {
|
|
111
|
-
console.log(JSON.stringify(doctorResult(), null, 2));
|
|
112
|
-
return "exit";
|
|
113
|
-
}
|
|
114
113
|
if (isHelpRequest(args)) {
|
|
115
114
|
console.log(helpText(command));
|
|
116
115
|
return "exit";
|
|
117
116
|
}
|
|
117
|
+
if (command === "doctor" || command === "--doctor") {
|
|
118
|
+
emitResult("doctor", doctorResult(), args);
|
|
119
|
+
return "exit";
|
|
120
|
+
}
|
|
118
121
|
try {
|
|
119
122
|
if (command === "install-skill" || command === "install_skill") {
|
|
120
123
|
console.log(JSON.stringify(await installSkill(args.slice(1)), null, 2));
|
|
@@ -286,15 +289,10 @@ async function handleCli(args) {
|
|
|
286
289
|
}
|
|
287
290
|
if (command === "plan" || command === "plan-integration") {
|
|
288
291
|
const input = await planCliInput(args);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
console.log(JSON.stringify(planSummary(payload), null, 2));
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
await printToolResult(planIntegrationTool, input);
|
|
297
|
-
}
|
|
292
|
+
const result = await planIntegrationTool.call(input);
|
|
293
|
+
const text = result.content.map((item) => item.text).join("\n");
|
|
294
|
+
const payload = JSON.parse(text);
|
|
295
|
+
emitResult("plan", hasFlag(args, "summary") ? planSummary(payload) : payload, args);
|
|
298
296
|
return "exit";
|
|
299
297
|
}
|
|
300
298
|
if (command === "plan-harness") {
|
|
@@ -418,21 +416,30 @@ async function handleCli(args) {
|
|
|
418
416
|
return "exit";
|
|
419
417
|
}
|
|
420
418
|
if (command === "init") {
|
|
421
|
-
assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake"], "init");
|
|
422
|
-
const
|
|
419
|
+
assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline"], "init");
|
|
420
|
+
const initRepo = positionalRepoPath(args.slice(1));
|
|
421
|
+
const result = await initCompliance(initRepo, requiredFlagValue(args, "request", "init requires --request."), flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake") });
|
|
423
422
|
if (result.status === "needs-clarification" && typeof result.exitCode === "number") {
|
|
424
423
|
process.exitCode = result.exitCode;
|
|
425
424
|
}
|
|
425
|
+
if (hasFlag(args, "baseline") && result.status === "initialized") {
|
|
426
|
+
result.baseline_recorded = await recordBaseline(initRepo);
|
|
427
|
+
}
|
|
426
428
|
console.log(JSON.stringify(result, null, 2));
|
|
427
429
|
return "exit";
|
|
428
430
|
}
|
|
429
431
|
if (command === "check") {
|
|
430
|
-
assertOnlyKnownFlags(args, ["ci"], "check");
|
|
431
|
-
const result = await checkCompliance(positionalRepoPath(args.slice(1)));
|
|
432
|
-
|
|
432
|
+
assertOnlyKnownFlags(args, ["ci", "format", "new-only"], "check");
|
|
433
|
+
const result = await checkCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only") });
|
|
434
|
+
emitResult("check", hasFlag(args, "ci") ? ciCheckResult(result) : result, args);
|
|
433
435
|
process.exitCode = result.exitCode;
|
|
434
436
|
return "exit";
|
|
435
437
|
}
|
|
438
|
+
if (command === "baseline") {
|
|
439
|
+
assertOnlyKnownFlags(args, [], "baseline");
|
|
440
|
+
console.log(JSON.stringify(await recordBaseline(positionalRepoPath(args.slice(1))), null, 2));
|
|
441
|
+
return "exit";
|
|
442
|
+
}
|
|
436
443
|
if (command === "sync") {
|
|
437
444
|
assertOnlyKnownFlags(args, [], "sync");
|
|
438
445
|
console.log(JSON.stringify(await syncCompliance(positionalRepoPath(args.slice(1))), null, 2));
|
|
@@ -452,13 +459,26 @@ async function handleCli(args) {
|
|
|
452
459
|
return "exit";
|
|
453
460
|
}
|
|
454
461
|
if (command === "explain") {
|
|
455
|
-
assertOnlyKnownFlags(args, [], "explain");
|
|
456
|
-
|
|
462
|
+
assertOnlyKnownFlags(args, ["format"], "explain");
|
|
463
|
+
const explainRuleId = positionalValues(args.slice(1))[0];
|
|
464
|
+
const payload = explainRuleId ? await explainRule(explainRuleId) : await listRules();
|
|
465
|
+
emitResult("explain", payload, args);
|
|
466
|
+
return "exit";
|
|
467
|
+
}
|
|
468
|
+
if (command === "explore") {
|
|
469
|
+
assertOnlyKnownFlags(args, ["request", "platform", "format"], "explore");
|
|
470
|
+
const request = flagValue(args, "request") ??
|
|
471
|
+
requiredPositionalText(args.slice(1), 'explore requires a request, e.g. `vise explore "add a social feed"`.');
|
|
472
|
+
emitResult("explore", exploreRequest(request, flagValue(args, "platform")), args);
|
|
457
473
|
return "exit";
|
|
458
474
|
}
|
|
459
475
|
if (command === "status") {
|
|
460
|
-
assertOnlyKnownFlags(args, [], "status");
|
|
461
|
-
|
|
476
|
+
assertOnlyKnownFlags(args, ["format", "new-only"], "status");
|
|
477
|
+
const result = await statusCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only") });
|
|
478
|
+
emitResult("status", result, args);
|
|
479
|
+
if (typeof result.exitCode === "number") {
|
|
480
|
+
process.exitCode = result.exitCode;
|
|
481
|
+
}
|
|
462
482
|
return "exit";
|
|
463
483
|
}
|
|
464
484
|
if (command === "engagement") {
|
|
@@ -595,7 +615,9 @@ Re-plan with collected answers (repeat --answer for each intake question):
|
|
|
595
615
|
vise plan . --request "Add a social feed" \\
|
|
596
616
|
--answer feed_scope=community \\
|
|
597
617
|
--answer feed_target=existing\\ communityId \\
|
|
598
|
-
--answer target_screen_or_route=app/feed/page.tsx
|
|
618
|
+
--answer target_screen_or_route=app/feed/page.tsx
|
|
619
|
+
|
|
620
|
+
Output is JSON by default (for agents/CI). Add --format human for a readable plan summary.`;
|
|
599
621
|
}
|
|
600
622
|
if (command === "creative") {
|
|
601
623
|
return `${packageName} creative
|
|
@@ -844,7 +866,9 @@ Usage:
|
|
|
844
866
|
Check the current source and recorded attestations against the compliance contract. Read-only.
|
|
845
867
|
|
|
846
868
|
Usage:
|
|
847
|
-
vise check [repoPath] [--ci]
|
|
869
|
+
vise check [repoPath] [--ci] [--format human]
|
|
870
|
+
|
|
871
|
+
Output is JSON by default (for agents/CI). Add --format human for a readable verdict summary.`;
|
|
848
872
|
}
|
|
849
873
|
if (command === "sync") {
|
|
850
874
|
return `${packageName} sync
|
|
@@ -860,23 +884,45 @@ Usage:
|
|
|
860
884
|
Record a host-agent or local-human attestation for one compliance rule.
|
|
861
885
|
|
|
862
886
|
Usage:
|
|
863
|
-
vise attest [repoPath] --rule
|
|
887
|
+
vise attest [repoPath] --rule typescript.client.region --confidence high --signer host-agent --evidence-file evidence.json --rationale "Why this rule is satisfied."`;
|
|
864
888
|
}
|
|
865
889
|
if (command === "explain") {
|
|
866
890
|
return `${packageName} explain
|
|
867
891
|
|
|
868
|
-
Explain one compliance rule.
|
|
892
|
+
Explain one compliance rule, or list every valid rule id when run with no id.
|
|
893
|
+
|
|
894
|
+
Usage:
|
|
895
|
+
vise explain [--format human] List all rule ids (public + contract) with where each applies
|
|
896
|
+
vise explain typescript.client.region [--format human]`;
|
|
897
|
+
}
|
|
898
|
+
if (command === "explore") {
|
|
899
|
+
return `${packageName} explore
|
|
900
|
+
|
|
901
|
+
Discover what social.plus offers for a request — before any project or credentials exist. Read-only:
|
|
902
|
+
maps the request to a candidate outcome (or lists the full menu when it can't), and for each shows the
|
|
903
|
+
capabilities involved, the canonical docs, and the command to start. No project, no API key, no writes.
|
|
869
904
|
|
|
870
905
|
Usage:
|
|
871
|
-
vise
|
|
906
|
+
vise explore "add a social feed" [--platform typescript] [--format human]`;
|
|
907
|
+
}
|
|
908
|
+
if (command === "doctor") {
|
|
909
|
+
return `${packageName} doctor
|
|
910
|
+
|
|
911
|
+
Print install diagnostics (Node version, transport, docs source, registered tools).
|
|
912
|
+
Read-only. JSON by default; --format human for a readable summary.
|
|
913
|
+
|
|
914
|
+
Usage:
|
|
915
|
+
vise doctor [--format human]
|
|
916
|
+
|
|
917
|
+
If diagnostics report a problem, see ${SUPPORT_URL}`;
|
|
872
918
|
}
|
|
873
919
|
if (command === "status") {
|
|
874
920
|
return `${packageName} status
|
|
875
921
|
|
|
876
|
-
Print a compact compliance summary.
|
|
922
|
+
Print a compact compliance summary. JSON by default; --format human for a readable verdict.
|
|
877
923
|
|
|
878
924
|
Usage:
|
|
879
|
-
vise status [repoPath]`;
|
|
925
|
+
vise status [repoPath] [--format human]`;
|
|
880
926
|
}
|
|
881
927
|
if (command === "design") {
|
|
882
928
|
return `${packageName} design
|
|
@@ -925,14 +971,17 @@ contractual record of which broad-social outcomes are in scope for this project.
|
|
|
925
971
|
|
|
926
972
|
Usage:
|
|
927
973
|
vise engagement init [repoPath] --tier <free|pro|partner> --customer-id <id> --scope <outcome,...>
|
|
928
|
-
vise engagement init [repoPath] --scope feed,
|
|
974
|
+
vise engagement init [repoPath] --scope add-feed,add-community --target-completion 2026-12-31 \\
|
|
929
975
|
--reviewer-name "Jane Doe" --reviewer-email jane@example.com --evidence-upload-consent
|
|
930
976
|
vise engagement show [repoPath]
|
|
931
977
|
|
|
932
978
|
Flags (init):
|
|
933
979
|
--tier <free|pro|partner> Engagement tier (validated).
|
|
934
980
|
--customer-id <id> Customer identifier recorded in the artifact.
|
|
935
|
-
--scope <outcome,...> In-scope
|
|
981
|
+
--scope <outcome,...> In-scope outcomes, comma-separated. One or more of: setup-sdk,
|
|
982
|
+
setup-push, setup-live-data, add-feed, add-comments, add-chat,
|
|
983
|
+
add-community, add-follow, add-moderation, add-notifications,
|
|
984
|
+
troubleshoot, validate-setup.
|
|
936
985
|
--target-completion <date> Target completion date, YYYY-MM-DD.
|
|
937
986
|
--reviewer-name <name> Human reviewer recorded on the engagement.
|
|
938
987
|
--reviewer-email <email> Human reviewer contact.
|
|
@@ -958,10 +1007,12 @@ Usage:
|
|
|
958
1007
|
vise learning record [repoPath] Record a local-only learning event
|
|
959
1008
|
vise learning show [repoPath] Show local learning summary
|
|
960
1009
|
vise debug [repoPath] --error ... Debug an SDK-specific runtime error and emit a repair brief
|
|
1010
|
+
vise explore "<request>" Discover what social.plus offers for a request (no project/credentials needed)
|
|
961
1011
|
vise plan [repoPath] --request "..." Create an implementation plan
|
|
962
1012
|
vise workplan next [repoPath] --request "..." Get the next broad-social surface to implement
|
|
963
|
-
vise init [repoPath] --request "..." Initialize compliance sidecar
|
|
964
|
-
vise check [repoPath] Check compliance contract
|
|
1013
|
+
vise init [repoPath] --request "..." Initialize compliance sidecar (add --baseline on a brownfield app)
|
|
1014
|
+
vise check [repoPath] Check compliance contract (add --new-only to gate on findings since the baseline)
|
|
1015
|
+
vise baseline [repoPath] Snapshot pre-existing findings so check --new-only gates only new ones
|
|
965
1016
|
vise sync [repoPath] Persist deterministic-pass evidence
|
|
966
1017
|
vise attest [repoPath] --rule ... Record a compliance attestation
|
|
967
1018
|
vise explain <ruleId> Explain one compliance rule
|
|
@@ -999,6 +1050,16 @@ async function printToolResult(tool, input) {
|
|
|
999
1050
|
console.log(text);
|
|
1000
1051
|
return { result, text };
|
|
1001
1052
|
}
|
|
1053
|
+
function emitResult(command, payload, args) {
|
|
1054
|
+
if (wantsHumanFormat(flagValue(args, "format"))) {
|
|
1055
|
+
const human = renderHuman(command, payload);
|
|
1056
|
+
if (human !== null) {
|
|
1057
|
+
console.log(human);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1062
|
+
}
|
|
1002
1063
|
async function planCliInput(args) {
|
|
1003
1064
|
const subArgs = args.slice(1);
|
|
1004
1065
|
const requestFromFlag = flagValue(args, "request");
|
|
@@ -1433,7 +1494,7 @@ async function completeWorkplanSurface(args) {
|
|
|
1433
1494
|
throw new Error(`Surface "${surfaceId}" is not in this social workplan. Available surfaces: ${sequence.map((item) => item.id).join(", ") || "(none)"}.`);
|
|
1434
1495
|
}
|
|
1435
1496
|
const now = new Date().toISOString();
|
|
1436
|
-
const check = await greenWorkplanCheck(repoRoot, surfaceId);
|
|
1497
|
+
const check = await greenWorkplanCheck(repoRoot, surfaceId, surface.outcome);
|
|
1437
1498
|
const snapshot = await writeWorkplanSurfaceSnapshot(repoRoot, surfaceId, check, now);
|
|
1438
1499
|
const existing = await readWorkplanProgress(repoRoot);
|
|
1439
1500
|
const base = existing?.request === request
|
|
@@ -1483,7 +1544,7 @@ async function completeWorkplanSurface(args) {
|
|
|
1483
1544
|
nextStep: status.nextStep,
|
|
1484
1545
|
};
|
|
1485
1546
|
}
|
|
1486
|
-
async function greenWorkplanCheck(repoRoot, surfaceId) {
|
|
1547
|
+
async function greenWorkplanCheck(repoRoot, surfaceId, expectedOutcome) {
|
|
1487
1548
|
let check;
|
|
1488
1549
|
try {
|
|
1489
1550
|
check = await checkCompliance(repoRoot);
|
|
@@ -1492,6 +1553,9 @@ async function greenWorkplanCheck(repoRoot, surfaceId) {
|
|
|
1492
1553
|
const message = error instanceof Error ? error.message : String(error);
|
|
1493
1554
|
throw new Error(`Cannot mark "${surfaceId}" complete because the current compliance check could not run. Run the focused \`vise init\` command for this surface first, then \`vise check .\`. ${message}`);
|
|
1494
1555
|
}
|
|
1556
|
+
if (expectedOutcome && check.outcome !== expectedOutcome) {
|
|
1557
|
+
throw new Error(`Cannot mark "${surfaceId}" complete: the sp-vise sidecar is currently scoped to outcome "${check.outcome}", not this surface's outcome "${expectedOutcome}". Each workplan surface has its own sidecar scope — run this surface's focused \`vise init\` command (it sets \`--answer feature_surface=${surfaceId}\`) and \`vise check .\` before recording it. (The check reported "${check.status}" only because it ran against the other surface's contract.)`);
|
|
1558
|
+
}
|
|
1495
1559
|
if (check.status !== "green") {
|
|
1496
1560
|
throw new Error(`Cannot mark "${surfaceId}" complete because \`vise check\` returned "${check.status}" (exit ${check.exitCode}). Resolve the check result before recording workplan progress.`);
|
|
1497
1561
|
}
|
|
@@ -1584,7 +1648,14 @@ function ciCheckResult(result) {
|
|
|
1584
1648
|
enabled: true,
|
|
1585
1649
|
passed: result.exitCode === 0,
|
|
1586
1650
|
exitCode: result.exitCode,
|
|
1587
|
-
blockingResultStatuses: [
|
|
1651
|
+
blockingResultStatuses: [
|
|
1652
|
+
"contract-drift",
|
|
1653
|
+
"blocked",
|
|
1654
|
+
"deterministic-failures",
|
|
1655
|
+
"needs-attestation",
|
|
1656
|
+
"completeness-gap",
|
|
1657
|
+
"selected-capability-failures",
|
|
1658
|
+
],
|
|
1588
1659
|
message: result.exitCode === 0 ? "Compliance green for CI." : `Compliance failed for CI with status: ${result.status}.`,
|
|
1589
1660
|
},
|
|
1590
1661
|
};
|
|
@@ -1594,7 +1665,7 @@ function positionalRepoPath(args) {
|
|
|
1594
1665
|
return values[0] ?? ".";
|
|
1595
1666
|
}
|
|
1596
1667
|
function positionalValues(args) {
|
|
1597
|
-
const flagsWithValues = new Set(["request", "requirements", "prototype", "variant", "variant-id", "brief", "brief-path", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source", "note", "kind", "sentiment", "metric"]);
|
|
1668
|
+
const flagsWithValues = new Set(["request", "requirements", "prototype", "variant", "variant-id", "brief", "brief-path", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source", "note", "kind", "sentiment", "metric", "tier", "customer-id", "scope", "target-completion", "reviewer-name", "reviewer-email"]);
|
|
1598
1669
|
const values = [];
|
|
1599
1670
|
for (let index = 0; index < args.length; index += 1) {
|
|
1600
1671
|
const arg = args[index];
|
|
@@ -1775,5 +1846,6 @@ function doctorResult() {
|
|
|
1775
1846
|
docsBaseUrl: process.env.SOCIAL_PLUS_DOCS_BASE_URL ?? "https://learn.social.plus",
|
|
1776
1847
|
transport: "stdio",
|
|
1777
1848
|
tools: Array.from(tools.keys()),
|
|
1849
|
+
support: SUPPORT_URL,
|
|
1778
1850
|
};
|
|
1779
1851
|
}
|
package/dist/solutionPath.js
CHANGED
|
@@ -56,7 +56,7 @@ const SDK_SIGNALS = [
|
|
|
56
56
|
id: "custom-ui",
|
|
57
57
|
label: "Custom UI or non-standard experience",
|
|
58
58
|
strength: "strong",
|
|
59
|
-
pattern: /\b(?:custom|bespoke|unique|fully[-\s]?custom|completely[-\s]?custom|totally[-\s]?(?:different|custom)|brand[-\s]?new|differentiated)\s+(?:(?:social|messaging|notification|community|chat|feed|profile|story|stories|comment|comments|user|group|channel|post|navigation|onboarding|discovery)\s+)?(?:ui|interface|experience)\b|\bnon[-\s]?standard (?:ui|layout|flow|experience)\b|\bpixel[-\s]?perfect\b/i,
|
|
59
|
+
pattern: /\b(?:custom|bespoke|unique|fully[-\s]?custom|completely[-\s]?custom|totally[-\s]?(?:different|custom)|brand[-\s]?new|differentiated)\s+(?:(?:social|messaging|notification|community|chat|feed|profile|story|stories|comment|comments|user|group|channel|post|navigation|onboarding|discovery)\s+)?(?:ui|interface|experience)\b|\b(?:our|my|their|its|your)\s+own\s+(?:\w+\s+){0,2}?(?:component(?:\s+tree)?|components?|screens?|widgets?|ui)\b|\bnon[-\s]?standard (?:ui|layout|flow|experience)\b|\bpixel[-\s]?perfect\b/i,
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
id: "custom-flows",
|