@amityco/social-plus-vise 1.3.0 → 1.4.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 +45 -2
- package/README.md +19 -9
- package/dist/capabilities.js +29 -1
- package/dist/entryState.js +71 -0
- package/dist/experience.js +70 -0
- package/dist/explore.js +1 -1
- package/dist/flow.js +382 -0
- package/dist/humanFormat.js +25 -0
- package/dist/intake.js +117 -0
- package/dist/intelligence/placement.js +2 -1
- package/dist/outcomes.js +126 -28
- package/dist/productExpectations.js +15 -0
- package/dist/requestReadiness.js +99 -0
- package/dist/server.js +566 -19
- package/dist/sidecar.js +5 -0
- package/dist/solutionPath.js +1 -1
- package/dist/tools/compliance.js +752 -125
- package/dist/tools/creative.js +14 -12
- package/dist/tools/design.js +321 -11
- package/dist/tools/experienceCompiler.js +1 -1
- package/dist/tools/experienceSensors.js +1 -1
- package/dist/tools/harness.js +13 -0
- package/dist/tools/integration.js +263 -90
- package/dist/tools/learning.js +1 -3
- package/dist/tools/project.js +963 -66
- package/dist/tools/smoke.js +134 -0
- package/dist/tools/uxHarness.js +54 -6
- package/dist/uikitCustomization.js +3 -1
- package/dist/version.js +7 -3
- package/package.json +1 -1
- package/packages/intelligence/catalog/catalog.schema.json +1 -1
- package/packages/intelligence/catalog/experience-objects.json +2 -2
- package/packages/intelligence/catalog/variants.json +24 -0
- package/rules/event.yaml +3 -0
- package/rules/feed.yaml +251 -0
- package/rules/invitation.yaml +4 -0
- package/rules/live-data.yaml +110 -0
- package/rules/notification-tray.yaml +4 -0
- package/rules/poll.yaml +5 -0
- package/rules/sdk-lifecycle.yaml +559 -0
- package/rules/search.yaml +5 -0
- package/rules/security.yaml +12 -0
- package/rules/story.yaml +5 -0
- package/rules/user-blocking.yaml +5 -0
- package/skills/social-plus-vise/SKILL.md +163 -15
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
|
|
4
|
+
function escapeRegExp(s) {
|
|
5
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6
|
+
}
|
|
7
|
+
function loadedCount(state) {
|
|
8
|
+
const match = state.match(/^loaded\b[\s\S]*?\bcount\s*=\s*(\d+)/i);
|
|
9
|
+
return match ? Number(match[1]) : null;
|
|
10
|
+
}
|
|
11
|
+
export function assessSmokeLog(logText, surfaces) {
|
|
12
|
+
const results = surfaces.map((surface) => {
|
|
13
|
+
const re = new RegExp(`VISE_SMOKE\\s+surface=${escapeRegExp(surface.id)}\\s+state=(.+)`, "g");
|
|
14
|
+
let match;
|
|
15
|
+
let last;
|
|
16
|
+
while ((match = re.exec(logText)) !== null)
|
|
17
|
+
last = match[1].trim();
|
|
18
|
+
if (last === undefined) {
|
|
19
|
+
return {
|
|
20
|
+
surface: surface.id,
|
|
21
|
+
state: "(no signal)",
|
|
22
|
+
verdict: "fail",
|
|
23
|
+
reason: "surface never emitted a VISE_SMOKE state — it did not mount, or the log marker is not wired",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const isLoaded = /^loaded\b/.test(last);
|
|
27
|
+
const isResolved = isLoaded || /^empty\b/.test(last);
|
|
28
|
+
const expect = surface.expect ?? "resolved";
|
|
29
|
+
const count = loadedCount(last);
|
|
30
|
+
const ok = expect === "populated" ? isLoaded && count !== null && count > 0 : isResolved;
|
|
31
|
+
if (ok)
|
|
32
|
+
return { surface: surface.id, state: last, verdict: "pass" };
|
|
33
|
+
let reason;
|
|
34
|
+
if (/^error/.test(last)) {
|
|
35
|
+
reason = "query errored before the session was ready (session-establish race) — drive it off a reactive readiness signal and retry the transient 'logging in' error";
|
|
36
|
+
}
|
|
37
|
+
else if (/^loading/.test(last)) {
|
|
38
|
+
reason = "surface never left loading — the query was created before the session was ready and did not recover";
|
|
39
|
+
}
|
|
40
|
+
else if (expect === "populated" && isLoaded && count === null) {
|
|
41
|
+
reason = "expected populated proof but the loaded state did not include count=N — emit the visible SDK item count";
|
|
42
|
+
}
|
|
43
|
+
else if (expect === "populated" && isLoaded) {
|
|
44
|
+
reason = `expected populated proof with count > 0 but loaded count=${count}`;
|
|
45
|
+
}
|
|
46
|
+
else if (expect === "populated") {
|
|
47
|
+
reason = "expected populated (loaded) but the surface resolved empty — verify data is present";
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
reason = `surface did not reach a resolved state (state=${last})`;
|
|
51
|
+
}
|
|
52
|
+
return { surface: surface.id, state: last, verdict: "fail", reason };
|
|
53
|
+
});
|
|
54
|
+
return { results, passed: results.length > 0 && results.every((r) => r.verdict === "pass") };
|
|
55
|
+
}
|
|
56
|
+
async function loadSmokeConfig(repoRoot) {
|
|
57
|
+
const file = path.join(repoRoot, "sp-vise", "smoke.json");
|
|
58
|
+
try {
|
|
59
|
+
const raw = await fs.readFile(file, "utf8");
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
if (!Array.isArray(parsed.surfaces))
|
|
62
|
+
return null;
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function smokeCaptureHelp() {
|
|
70
|
+
return [
|
|
71
|
+
"Create `sp-vise/smoke.json` with the mounted collection surfaces, for example `{ \"platform\": \"android\", \"surfaces\": [{ \"id\": \"feed\", \"expect\": \"populated\" }] }`.",
|
|
72
|
+
"Emit `VISE_SMOKE surface=<id> state=loading|empty|loaded count=N|error:<message>` from the real SDK query lifecycle; `expect: \"populated\"` requires loaded count=N with N > 0.",
|
|
73
|
+
"Android: clear logcat before launch, cold-launch, wait for resolution, then `adb logcat -d -v time | grep VISE_SMOKE > sp-vise/evidence/runtime-smoke.log`.",
|
|
74
|
+
"iOS: after `simctl launch`, run `xcrun simctl spawn <device> log show --style compact --last 3m --predicate 'eventMessage CONTAINS \"VISE_SMOKE\"' > sp-vise/evidence/runtime-smoke.log`.",
|
|
75
|
+
"Then run `vise smoke . --log sp-vise/evidence/runtime-smoke.log` and rerun `vise check --ci`.",
|
|
76
|
+
].join(" ");
|
|
77
|
+
}
|
|
78
|
+
export const smokeTool = {
|
|
79
|
+
name: "smoke",
|
|
80
|
+
description: "Assess a captured runtime mount-smoke log (VISE_SMOKE markers) into a pass/fail verdict and record runtime-smoke evidence. Catches the session-establish race that static check cannot.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
repoPath: { type: "string", description: "Absolute or relative path to the customer repository root." },
|
|
85
|
+
logPath: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "Path to a captured runtime log containing `VISE_SMOKE surface=<id> state=<…>` markers (produced by the platform capture harness, e.g. bench/ios-runtime-smoke.sh, cold-launching into each collection surface).",
|
|
88
|
+
},
|
|
89
|
+
surface: { type: "string", description: "Optional: assess only this surface id from smoke.json." },
|
|
90
|
+
},
|
|
91
|
+
required: ["repoPath", "logPath"],
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
},
|
|
94
|
+
async call(input) {
|
|
95
|
+
const args = objectInput(input);
|
|
96
|
+
const repoRoot = path.resolve(stringField(args, "repoPath"));
|
|
97
|
+
const logPath = path.resolve(repoRoot, stringField(args, "logPath"));
|
|
98
|
+
const only = optionalStringField(args, "surface");
|
|
99
|
+
const config = await loadSmokeConfig(repoRoot);
|
|
100
|
+
if (!config) {
|
|
101
|
+
return textResult({
|
|
102
|
+
status: "no-config",
|
|
103
|
+
note: `No sp-vise/smoke.json found. ${smokeCaptureHelp()}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
let logText;
|
|
107
|
+
try {
|
|
108
|
+
logText = await fs.readFile(logPath, "utf8");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return textResult({ status: "no-log", note: `Could not read captured log at ${logPath}. ${smokeCaptureHelp()}` });
|
|
112
|
+
}
|
|
113
|
+
const surfaces = only ? config.surfaces.filter((s) => s.id === only) : config.surfaces;
|
|
114
|
+
if (surfaces.length === 0) {
|
|
115
|
+
return textResult({ status: "no-surfaces", note: only ? `Surface '${only}' is not declared in smoke.json.` : "smoke.json declares no surfaces." });
|
|
116
|
+
}
|
|
117
|
+
const assessment = assessSmokeLog(logText, surfaces);
|
|
118
|
+
try {
|
|
119
|
+
const evidenceDir = path.join(repoRoot, "sp-vise", "evidence");
|
|
120
|
+
await fs.mkdir(evidenceDir, { recursive: true });
|
|
121
|
+
await fs.writeFile(path.join(evidenceDir, "runtime-smoke.json"), JSON.stringify({ passed: assessment.passed, platform: config.platform, results: assessment.results }, null, 2));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
}
|
|
125
|
+
return textResult({
|
|
126
|
+
status: assessment.passed ? "passed" : "failed",
|
|
127
|
+
platform: config.platform,
|
|
128
|
+
results: assessment.results,
|
|
129
|
+
...(assessment.passed
|
|
130
|
+
? {}
|
|
131
|
+
: { note: "Runtime mount-smoke FAILED — at least one collection surface did not reach a resolved state on cold-launch (session-establish race). This is invisible to `vise check`." }),
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
};
|
package/dist/tools/uxHarness.js
CHANGED
|
@@ -111,6 +111,7 @@ export function uxHarnessPlanContext(harness, outcome) {
|
|
|
111
111
|
antiPatterns,
|
|
112
112
|
implementationGuidance: [
|
|
113
113
|
`Preserve the selected variant's UX pattern(s): ${harness.uxPatterns.map((pattern) => pattern.name).join(", ") || "(none)"}.`,
|
|
114
|
+
"Keep Vise, SDK, intake, scope, dogfood, and auth/debug vocabulary in implementation notes, evidence, or dev-only diagnostics; visible app copy should use the host product's language.",
|
|
114
115
|
...expectations.slice(0, 6).map((expectation) => `${expectation.title}: ${expectation.guidance}`),
|
|
115
116
|
...(antiPatterns.length > 0
|
|
116
117
|
? [`Avoid these UX anti-patterns: ${antiPatterns.slice(0, 4).map((item) => item.warning).join("; ")}.`]
|
|
@@ -142,6 +143,7 @@ export async function assessUxHarness(effectiveRoot, harness, outcome) {
|
|
|
142
143
|
};
|
|
143
144
|
}
|
|
144
145
|
const corpus = await collectSourceCorpus(effectiveRoot);
|
|
146
|
+
const copyRisks = productionCopyRisksFor(corpus);
|
|
145
147
|
const assessed = expectations.map((expectation) => {
|
|
146
148
|
const evidence = evidenceFor(expectation, corpus);
|
|
147
149
|
return {
|
|
@@ -158,7 +160,10 @@ export async function assessUxHarness(effectiveRoot, harness, outcome) {
|
|
|
158
160
|
acc[item.status] = (acc[item.status] ?? 0) + 1;
|
|
159
161
|
return acc;
|
|
160
162
|
}, {});
|
|
161
|
-
|
|
163
|
+
if (copyRisks.length > 0) {
|
|
164
|
+
summary["copy-risk"] = copyRisks.length;
|
|
165
|
+
}
|
|
166
|
+
const status = copyRisks.length > 0 || assessed.some((item) => item.status === "needs-review" && item.priority === "high")
|
|
162
167
|
? "needs-review"
|
|
163
168
|
: "evidence-found";
|
|
164
169
|
return {
|
|
@@ -166,9 +171,10 @@ export async function assessUxHarness(effectiveRoot, harness, outcome) {
|
|
|
166
171
|
harnessPath: "sp-vise/ux-harness.json",
|
|
167
172
|
advisoryOnly: advisoryOnlyText(),
|
|
168
173
|
assessedExpectations: assessed,
|
|
174
|
+
...(copyRisks.length > 0 ? { copyRisks } : {}),
|
|
169
175
|
summary,
|
|
170
176
|
nextStep: status === "needs-review"
|
|
171
|
-
? "Review UX Harness needs-review items with the user or host agent. They are advisory and do not affect `vise check` exit codes."
|
|
177
|
+
? "Review UX Harness needs-review items and production-copy risks with the user or host agent. They are advisory and do not affect `vise check` exit codes."
|
|
172
178
|
: "UX Harness advisory evidence was found. Keep reviewing visually before claiming production UX quality.",
|
|
173
179
|
};
|
|
174
180
|
}
|
|
@@ -224,13 +230,13 @@ async function readCreativeSelectionSidecar(repoRoot) {
|
|
|
224
230
|
raw = await readFile(filePath, "utf8");
|
|
225
231
|
}
|
|
226
232
|
catch {
|
|
227
|
-
throw new Error(`Unable to read ${rel}. Run \`vise creative accept . --variant <id
|
|
233
|
+
throw new Error(`Unable to read ${rel}. Run \`vise creative accept . --variant <id> --rationale "<why>" --confidence high\` first.`);
|
|
228
234
|
}
|
|
229
235
|
try {
|
|
230
236
|
return JSON.parse(raw);
|
|
231
237
|
}
|
|
232
238
|
catch {
|
|
233
|
-
throw new Error(`${rel} is present but could not be parsed (corrupt JSON). Re-run \`vise creative accept . --variant <id
|
|
239
|
+
throw new Error(`${rel} is present but could not be parsed (corrupt JSON). Re-run \`vise creative accept . --variant <id> --rationale "<why>" --confidence high\` to regenerate it.`);
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
async function writeUxHarness(repoRoot, harness) {
|
|
@@ -331,6 +337,7 @@ function surfaceIdForOutcome(outcome) {
|
|
|
331
337
|
async function collectSourceCorpus(root) {
|
|
332
338
|
const filesByHint = new Map();
|
|
333
339
|
const chunks = [];
|
|
340
|
+
const corpusFiles = [];
|
|
334
341
|
const files = await sourceFiles(root);
|
|
335
342
|
for (const file of files) {
|
|
336
343
|
const text = await readFile(file, "utf8").catch(() => "");
|
|
@@ -339,6 +346,7 @@ async function collectSourceCorpus(root) {
|
|
|
339
346
|
}
|
|
340
347
|
const lower = text.toLowerCase();
|
|
341
348
|
chunks.push(lower);
|
|
349
|
+
corpusFiles.push({ path: repoRelativePath(root, file), lower });
|
|
342
350
|
for (const hint of UX_EVIDENCE_HINTS) {
|
|
343
351
|
if (!lower.includes(hint)) {
|
|
344
352
|
continue;
|
|
@@ -349,7 +357,7 @@ async function collectSourceCorpus(root) {
|
|
|
349
357
|
filesByHint.set(hint, existing);
|
|
350
358
|
}
|
|
351
359
|
}
|
|
352
|
-
return { text: chunks.join("\n"), filesByHint };
|
|
360
|
+
return { text: chunks.join("\n"), filesByHint, files: corpusFiles };
|
|
353
361
|
}
|
|
354
362
|
async function sourceFiles(root) {
|
|
355
363
|
const result = [];
|
|
@@ -395,6 +403,32 @@ function evidenceFor(expectation, corpus) {
|
|
|
395
403
|
}
|
|
396
404
|
return { hints, files: [...files].sort().slice(0, 8) };
|
|
397
405
|
}
|
|
406
|
+
function productionCopyRisksFor(corpus) {
|
|
407
|
+
const filesByTerm = new Map();
|
|
408
|
+
for (const file of corpus.files) {
|
|
409
|
+
for (const term of PRODUCTION_COPY_RISK_TERMS) {
|
|
410
|
+
if (!file.lower.includes(term)) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const existing = filesByTerm.get(term) ?? new Set();
|
|
414
|
+
existing.add(file.path);
|
|
415
|
+
filesByTerm.set(term, existing);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const riskyTerms = [...filesByTerm.keys()].sort();
|
|
419
|
+
if (riskyTerms.length === 0) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
return [
|
|
423
|
+
{
|
|
424
|
+
status: "needs-review",
|
|
425
|
+
title: "Visible app copy may expose Vise planning or debug vocabulary",
|
|
426
|
+
terms: riskyTerms,
|
|
427
|
+
files: [...new Set(riskyTerms.flatMap((term) => [...(filesByTerm.get(term) ?? [])]))].sort().slice(0, 12),
|
|
428
|
+
guidance: "Rewrite customer-visible labels and descriptions into the host product language, and move SDK/session/scope/dogfood details to comments, handoff notes, or dev-only diagnostics.",
|
|
429
|
+
},
|
|
430
|
+
];
|
|
431
|
+
}
|
|
398
432
|
function projectSummary(inspection) {
|
|
399
433
|
return {
|
|
400
434
|
repoRoot: inspection.repoRoot,
|
|
@@ -475,11 +509,12 @@ const UX_PATTERN_PRESETS = {
|
|
|
475
509
|
expectations: [
|
|
476
510
|
expectation("embedded-feed-context", "Feed appears near the host context it supports", "high", ["feed"], "Place the feed close to the product, content, home, or detail context it enriches.", ["feed", "postrepository", "targetid", "targettype", "communityid", "userid"]),
|
|
477
511
|
expectation("embedded-feed-composer-scope", "Feed composer or read-only scope is explicit", "high", ["feed"], "If creation is in scope, expose a composer with clear target scope; otherwise state that the feed is read-only.", ["composer", "createpost", "readonly", "read only", "postrepository.createpost"]),
|
|
512
|
+
expectation("embedded-feed-post-body", "Post cards render the body content", "high", ["feed"], "Render each post's normal body/description/caption when present instead of making postId or counts the primary card content. A title field is optional; do not require metadata/custom title payloads just to satisfy rendering.", ["post.data.text", "gettext", "description", "caption", "postbody", "postcontent"]),
|
|
478
513
|
expectation("embedded-feed-rich-post-rendering", "Rich post types render as rich content when in scope", "medium", ["feed"], "If the feed includes image, video, file, poll, or shared parent-child posts, render them as rich content with media/poll/quoted UI and loading/fallback states instead of flattening every post to plain text. The deterministic feed.rich-post-rendering sensor (parent-child-rendered / post-datatype-handled) provides platform evidence.", ["getimageinfo", "getfileinfo", "fileinfo", "imageinfo", "getpoll", "poll", "parent", "children", "mediarepository", "thumbnail"]),
|
|
479
514
|
expectation("embedded-feed-rich-post-create", "Composer surfaces rich post create affordances when in scope", "medium", ["feed"], "If image, file, or poll creation is in scope, surface those affordances in the composer rather than only a plain text input. The deterministic feed.rich-post-composer-scope sensor provides platform evidence.", ["imagepicker", "pickimage", "mediapicker", "createpoll", "create poll", "addimage", "imageupload", "attachimage"]),
|
|
480
515
|
],
|
|
481
516
|
tradeoffs: ["Fits existing journeys without adding navigation", "Can be overlooked if buried too far below primary content"],
|
|
482
|
-
antiPatterns: ["Feed disconnected from the host object", "Composer creates into an unclear target", "Rich posts (image/video/poll/parent-child) flattened to plain text", "Composer limited to text when richer creation is in scope"],
|
|
517
|
+
antiPatterns: ["Feed disconnected from the host object", "Composer creates into an unclear target", "Vise/intake/debug labels exposed as visible product copy", "Rich posts (image/video/poll/parent-child) flattened to plain text", "Composer limited to text when richer creation is in scope"],
|
|
483
518
|
},
|
|
484
519
|
"contextual-comment-tray": {
|
|
485
520
|
expectations: [
|
|
@@ -547,3 +582,16 @@ const UX_PATTERN_PRESETS = {
|
|
|
547
582
|
},
|
|
548
583
|
};
|
|
549
584
|
const UX_EVIDENCE_HINTS = Array.from(new Set(Object.values(UX_PATTERN_PRESETS).flatMap((preset) => preset.expectations.flatMap((expectation) => expectation.evidenceHints)))).sort();
|
|
585
|
+
const PRODUCTION_COPY_RISK_TERMS = [
|
|
586
|
+
"composer scope",
|
|
587
|
+
"current signed-in social plus",
|
|
588
|
+
"dev/testing",
|
|
589
|
+
"dogfood",
|
|
590
|
+
"engagement scope",
|
|
591
|
+
"feed target resolved",
|
|
592
|
+
"post type scope",
|
|
593
|
+
"resolved against",
|
|
594
|
+
"sdk path",
|
|
595
|
+
"social plus user feed",
|
|
596
|
+
"target: your current social plus",
|
|
597
|
+
].sort();
|
|
@@ -88,7 +88,8 @@ export function recommendUIKitCustomization(args) {
|
|
|
88
88
|
const solutionNeedsUIKitAttention = args.solutionPath?.recommendation === "uikit" ||
|
|
89
89
|
args.solutionPath?.recommendation === "hybrid" ||
|
|
90
90
|
args.solutionPath?.recommendation === "needs-decision";
|
|
91
|
-
|
|
91
|
+
const sp = args.solutionPath;
|
|
92
|
+
if (!explicitAnswer && sp?.recommendation === "sdk" && (sp.signals.sdk.length > 0 || !hasCustomizationSignal)) {
|
|
92
93
|
return undefined;
|
|
93
94
|
}
|
|
94
95
|
if (!explicitAnswer && !unrecognizedExplicitAnswer && (!hasCustomizationSignal || sdkCustomUiOnly) && !solutionNeedsUIKitAttention) {
|
|
@@ -331,6 +332,7 @@ function platformGuidanceFor(platform) {
|
|
|
331
332
|
return [
|
|
332
333
|
"Web/TypeScript UIKit: pass local config through `AmityUIKitProvider configs`, enable `syncNetworkConfig` for Dynamic UI, pass `pageBehavior` for navigation/action overrides, and use the `localization` prop for locale bundles or string overrides.",
|
|
333
334
|
"Web source anchors: `Amity-Social-Cloud-UIKit-Web/readme.md`, `src/v4/core/providers/AmityUIKitProvider.tsx`, `src/v4/core/providers/CustomizationProvider`, `src/v4/core/providers/PageBehaviorProvider.tsx`, and `src/v4/constants/customization.ts`.",
|
|
335
|
+
"Web/TypeScript UIKit install caveats (npm + bundler): (1) the published UIKit (`@amityco/ui-kit-open-source`) has peer-dependency ranges that conflict with `@amityco/ts-sdk`, so `npm install` may need `--legacy-peer-deps`; (2) it pulls `react-intl`, which requires `tslib` — install `tslib` explicitly, or a modern bundler (Vite 8 / rolldown, esbuild) hard-fails the build with an unresolved `tslib`; (3) a partial `configs` theme can trip the exported `ConfigurableThemeValue` type (it expects the full color-slot set) — provide all slots or cast. These are install/build-time only and do not affect compliance.",
|
|
334
336
|
];
|
|
335
337
|
}
|
|
336
338
|
function labelForLevel(level) {
|
package/dist/version.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
4
5
|
function readPackageMetadata() {
|
|
5
|
-
const
|
|
6
|
-
const packageJsonPath = path.resolve(moduleDir, "..", "package.json");
|
|
6
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
7
7
|
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
8
8
|
if (typeof parsed.name !== "string" || typeof parsed.version !== "string") {
|
|
9
9
|
throw new Error(`Invalid package metadata in ${packageJsonPath}.`);
|
|
@@ -17,3 +17,7 @@ const packageMetadata = readPackageMetadata();
|
|
|
17
17
|
export const packageName = packageMetadata.name;
|
|
18
18
|
export const packageVersion = packageMetadata.version;
|
|
19
19
|
export const packageUserAgent = `social-plus-vise/${packageVersion}`;
|
|
20
|
+
export function detectBuildSource(root) {
|
|
21
|
+
return existsSync(path.join(root, "src")) ? "local" : "published";
|
|
22
|
+
}
|
|
23
|
+
export const buildSource = detectBuildSource(packageRoot);
|
package/package.json
CHANGED
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"description": "Free vocabulary today (the guard does not enum it). Current values: content, participation, knowledge, identity, conversation, live, commitment, progression, incentive, recognition, competition, activation."
|
|
96
96
|
},
|
|
97
97
|
"surface": {
|
|
98
|
-
"enum": ["feed", "comments", "chat", "profile", "community", "notifications"],
|
|
98
|
+
"enum": ["feed", "livestream", "comments", "chat", "profile", "community", "notifications"],
|
|
99
99
|
"description": "UI surface this object is placed on. Present only on SDK-backed objects (its presence is what makes an object SOCIAL_PLUS_OBJECTS). Source of truth for outcomesForObjects/surfaceIdsForObjects/CREATIVE_SURFACE_HINTS, derived in src/intelligence/placement.ts. The surface set is closed (each is bound to an Outcome in code) — adding a new surface is an engine decision, not a catalog edit."
|
|
100
100
|
},
|
|
101
101
|
"block": {
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"items": [
|
|
5
5
|
{ "id": "feed", "name": "Feed", "description": "A stream of social posts or updates.", "category": "content", "surface": "feed", "block": "feed" },
|
|
6
6
|
{ "id": "comment", "name": "Comment", "description": "A response thread attached to content or an object.", "category": "participation", "surface": "comments", "block": "comments" },
|
|
7
|
-
{ "id": "forum", "name": "Forum", "description": "A structured discussion space optimized for topics and knowledge.", "category": "knowledge", "surface": "
|
|
7
|
+
{ "id": "forum", "name": "Forum", "description": "A structured discussion space optimized for topics and knowledge.", "category": "knowledge", "surface": "feed" },
|
|
8
8
|
{ "id": "community", "name": "Community", "description": "A persistent group identity around people, content, or purpose.", "category": "identity", "surface": "community", "block": "community" },
|
|
9
9
|
{ "id": "creator", "name": "Creator", "description": "A person or entity that owns an audience relationship.", "category": "identity", "surface": "profile", "block": "profile" },
|
|
10
10
|
{ "id": "profile", "name": "Profile", "description": "A user or creator identity surface with social relationship signals.", "category": "identity", "surface": "profile", "block": "profile" },
|
|
11
11
|
{ "id": "chat", "name": "Chat", "description": "Direct or group conversation.", "category": "conversation", "surface": "chat", "block": "chat" },
|
|
12
12
|
{ "id": "story", "name": "Story", "description": "Ephemeral or sequenced media updates.", "category": "content", "surface": "feed" },
|
|
13
|
-
{ "id": "livestream", "name": "Livestream", "description": "Real-time video or audio participation.", "category": "live", "surface": "
|
|
13
|
+
{ "id": "livestream", "name": "Livestream", "description": "Real-time video or audio participation.", "category": "live", "surface": "livestream", "block": "livestream" },
|
|
14
14
|
{ "id": "event", "name": "Event", "description": "A scheduled social moment.", "category": "live", "surface": "notifications", "block": "events" },
|
|
15
15
|
{ "id": "rsvp", "name": "RSVP", "description": "A user's intent to attend or participate in an event.", "category": "commitment", "surface": "notifications", "block": "events" },
|
|
16
16
|
{ "id": "challenge", "name": "Challenge", "description": "A time-bounded task or contest.", "category": "participation", "availability": "in-development" },
|
|
@@ -127,6 +127,30 @@
|
|
|
127
127
|
"uxPatternIds": ["dedicated-community-tab", "event-center"],
|
|
128
128
|
"tradeoffs": ["serves fitness retention today on shipped primitives, without gamification", "competition/streak mechanics arrive when social.plus gamification ships"],
|
|
129
129
|
"whenToChoose": "Choose for a fitness or wellness community on shipped primitives: group classes, live workout sessions, an events calendar, and a space for peer accountability with member profiles. Best when retention comes from showing up together, with no competition or streak/points mechanics."
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "messaging-first",
|
|
133
|
+
"name": "Messaging First",
|
|
134
|
+
"description": "Make real-time conversation the primary social surface: 1:1 and group messaging with an inbox as the home base.",
|
|
135
|
+
"businessGoalIds": ["participation", "retention"],
|
|
136
|
+
"archetypeIds": ["enterprise", "gaming", "media-streaming", "education"],
|
|
137
|
+
"solutionPatternIds": ["peer-support", "live-participation"],
|
|
138
|
+
"experienceObjectIds": ["chat", "profile", "notification", "community"],
|
|
139
|
+
"uxPatternIds": ["inbox-first-chat"],
|
|
140
|
+
"tradeoffs": ["fastest path to daily active conversation and retention", "needs presence/notification investment and moderation for direct messages"],
|
|
141
|
+
"whenToChoose": "Choose when direct conversation is the primary destination: 1:1 and group chat, messaging/DMs, an inbox or threads as the home surface, real-time presence and typing. Best when people come to talk to each other rather than to browse a feed."
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "social-graph-first",
|
|
145
|
+
"name": "Social Graph First",
|
|
146
|
+
"description": "Make following people and creators the backbone: rich profiles and a follow/unfollow graph that powers a personalized following feed.",
|
|
147
|
+
"businessGoalIds": ["creator-growth", "retention", "participation"],
|
|
148
|
+
"archetypeIds": ["creator", "media-streaming", "fitness", "loyalty"],
|
|
149
|
+
"solutionPatternIds": ["audience-ownership", "recognition"],
|
|
150
|
+
"experienceObjectIds": ["profile", "creator", "feed", "notification"],
|
|
151
|
+
"uxPatternIds": ["profile-social-header"],
|
|
152
|
+
"tradeoffs": ["personalized, relationship-driven distribution that compounds with each follow", "cold-start risk until users follow enough accounts to fill the following feed"],
|
|
153
|
+
"whenToChoose": "Choose when the follow relationship is the backbone of the experience: user and creator profiles, follow/unfollow, a personalized following feed, and follower-driven notifications. Best when distribution should come from who you follow rather than from a single shared community feed."
|
|
130
154
|
}
|
|
131
155
|
]
|
|
132
156
|
}
|
package/rules/event.yaml
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
{
|
|
6
6
|
"id": "typescript.event.live-collection",
|
|
7
7
|
"version": 1,
|
|
8
|
+
"symbol_anchored": { "platform": "typescript", "types": ["EventRepository"], "members": ["getEvents"], "basis": "names-only" },
|
|
8
9
|
"title": "TypeScript event calendars / attendee lists should use the reactive Event LiveCollection",
|
|
9
10
|
"severity": "warning",
|
|
10
11
|
"rationale": "Event calendars (getEvents) and attendee/RSVP lists (getRSVPs) are Live Collections — rsvpCount and the going/not-going roster update as people RSVP. A one-shot fetch (no callback) renders a list that never updates. Pass the live callback: EventRepository.getEvents(params, callback).",
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
{
|
|
19
20
|
"id": "android.event.live-collection",
|
|
20
21
|
"version": 1,
|
|
22
|
+
"symbol_anchored": { "platform": "android", "types": ["AmityEventRepository"], "members": ["getEvents"], "basis": "names-only" },
|
|
21
23
|
"title": "Android event calendars / attendee lists should use the reactive Event LiveCollection",
|
|
22
24
|
"severity": "warning",
|
|
23
25
|
"rationale": "Event calendars (getEvents) and attendee/RSVP lists (getRSVPs) are Live Collections — rsvpCount and the going/not-going roster update as people RSVP. A one-shot fetch renders a list that never updates. Observe the collection instead (PagingData via .doOnNext { ... }.subscribe(), or getRSVPs(...).observe()).",
|
|
@@ -31,6 +33,7 @@
|
|
|
31
33
|
{
|
|
32
34
|
"id": "ios.event.live-collection",
|
|
33
35
|
"version": 1,
|
|
36
|
+
"symbol_anchored": { "platform": "ios", "types": ["AmityEventRepository"], "members": ["getEvents"], "basis": "names-only" },
|
|
34
37
|
"title": "iOS event calendars / attendee lists should use the reactive Event LiveCollection",
|
|
35
38
|
"severity": "warning",
|
|
36
39
|
"rationale": "Event calendars (getEvents) and attendee/RSVP lists (getRSVPs) are Live Collections — rsvpCount and the going/not-going roster update as people RSVP. A one-shot snapshot renders a list that never updates. Observe the collection instead: getEvents(...).observe { ... } / getRSVPs(...).observe { ... }.",
|