@amityco/social-plus-vise 1.2.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 +75 -2
- package/README.md +23 -10
- package/dist/capabilities.js +35 -4
- package/dist/entryState.js +71 -0
- package/dist/experience.js +70 -0
- package/dist/explore.js +51 -0
- package/dist/flow.js +382 -0
- package/dist/humanFormat.js +251 -0
- package/dist/intake.js +117 -0
- package/dist/intelligence/placement.js +2 -1
- package/dist/outcomes.js +141 -42
- package/dist/productExpectations.js +15 -0
- package/dist/requestReadiness.js +99 -0
- package/dist/server.js +675 -56
- package/dist/sidecar.js +5 -0
- package/dist/solutionPath.js +2 -2
- package/dist/tools/compliance.js +1014 -133
- package/dist/tools/creative.js +14 -12
- package/dist/tools/debug.js +83 -26
- package/dist/tools/design.js +344 -14
- 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 +987 -72
- package/dist/tools/sdkFacts.js +8 -1
- package/dist/tools/smoke.js +134 -0
- package/dist/tools/uxHarness.js +54 -6
- package/dist/uikitCustomization.js +22 -6
- 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
package/dist/flow.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { sidecarDir } from "./sidecar.js";
|
|
5
|
+
export function blueprintDigest(input) {
|
|
6
|
+
const canonical = JSON.stringify({
|
|
7
|
+
path: input.path ?? null,
|
|
8
|
+
stages: input.stages.map((stage) => ({ id: stage.id, outcome: stage.outcome })),
|
|
9
|
+
design: input.design ?? null,
|
|
10
|
+
});
|
|
11
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
12
|
+
}
|
|
13
|
+
export function flowStatePath(repoRoot) {
|
|
14
|
+
return path.join(sidecarDir(repoRoot), "flow.json");
|
|
15
|
+
}
|
|
16
|
+
export function flowBlueprintPath(repoRoot) {
|
|
17
|
+
return path.join(sidecarDir(repoRoot), "flow-blueprint.html");
|
|
18
|
+
}
|
|
19
|
+
export async function readFlowState(repoRoot) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(await readFile(flowStatePath(repoRoot), "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function writeFlowState(repoRoot, state) {
|
|
28
|
+
const target = flowStatePath(repoRoot);
|
|
29
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
30
|
+
await writeFile(target, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
31
|
+
}
|
|
32
|
+
export async function readOmittedSurfaces(repoRoot, request) {
|
|
33
|
+
const state = await readFlowState(repoRoot);
|
|
34
|
+
if (!state || state.request !== request) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
return state.blueprint?.omitted ?? [];
|
|
38
|
+
}
|
|
39
|
+
function escapeHtml(value) {
|
|
40
|
+
return value
|
|
41
|
+
.replace(/&/g, "&")
|
|
42
|
+
.replace(/</g, "<")
|
|
43
|
+
.replace(/>/g, ">")
|
|
44
|
+
.replace(/"/g, """);
|
|
45
|
+
}
|
|
46
|
+
function surfaceReadiness(stage) {
|
|
47
|
+
const id = stage.id || stage.outcome;
|
|
48
|
+
const label = stage.label ?? stage.id;
|
|
49
|
+
const base = {
|
|
50
|
+
id,
|
|
51
|
+
label,
|
|
52
|
+
seedGuidance: "If tenant data is missing, create safe dogfood data through Console or SDK seed functions, then use the returned SDK object in the running app instead of copying its ID into source/config.",
|
|
53
|
+
};
|
|
54
|
+
switch (id) {
|
|
55
|
+
case "community":
|
|
56
|
+
return {
|
|
57
|
+
...base,
|
|
58
|
+
smokeExpectation: "populated",
|
|
59
|
+
smokeReason: "Community discovery is only proven when at least one readable community renders.",
|
|
60
|
+
targetSources: [
|
|
61
|
+
"SDK community discovery/search/recommended query scoped to the current user.",
|
|
62
|
+
"Host route or user selection when the product already has a community picker/detail route.",
|
|
63
|
+
"SDK create-community seed flow; use the returned community object immediately for the dogfood journey.",
|
|
64
|
+
],
|
|
65
|
+
tenantData: ["At least one public or dogfood-readable community exists and is discoverable by the current user."],
|
|
66
|
+
consolePrep: ["Confirm community visibility, membership permissions, moderation/review mode, and role access for the dogfood user."],
|
|
67
|
+
noDataFallback: [
|
|
68
|
+
"Ask whether to create a dogfood community in Console or through the SDK create-community flow.",
|
|
69
|
+
"After creating it, re-run discovery and select the rendered/returned community naturally.",
|
|
70
|
+
"If seeding is declined, only a resolved empty-state proof is possible; do not claim populated community proof.",
|
|
71
|
+
],
|
|
72
|
+
evidenceToRecord: ["Selected community display name and resolved communityId in smoke evidence, not source/config."],
|
|
73
|
+
};
|
|
74
|
+
case "feed":
|
|
75
|
+
return {
|
|
76
|
+
...base,
|
|
77
|
+
smokeExpectation: "populated",
|
|
78
|
+
smokeReason: "A feed integration is only product-proven when visible posts render, not just an empty shell.",
|
|
79
|
+
targetSources: [
|
|
80
|
+
"Current-user or global feed when the selected product story supports it.",
|
|
81
|
+
"Selected community returned by the community discovery/create flow.",
|
|
82
|
+
"Selected profile/user object from host auth, profile, member, follower, or search state.",
|
|
83
|
+
],
|
|
84
|
+
tenantData: ["A target global, community, or user feed is known, and it contains at least one visible post for the current user."],
|
|
85
|
+
consolePrep: ["Confirm post permissions, community privacy, review mode, and any selected post-type availability."],
|
|
86
|
+
noDataFallback: [
|
|
87
|
+
"Ask whether to create a visible dogfood post in the selected runtime feed target through Console or SDK post creation.",
|
|
88
|
+
"Use the SDK return value or the next feed query result as the target for composer/comment proof.",
|
|
89
|
+
"If no post can be created or discovered, record that populated feed proof was not run.",
|
|
90
|
+
],
|
|
91
|
+
evidenceToRecord: ["Rendered feed target type, target display name, resolved target ID, and visible post count."],
|
|
92
|
+
};
|
|
93
|
+
case "comments":
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
smokeExpectation: "populated",
|
|
97
|
+
smokeReason: "Comments need a known parent post/thread so read and write affordances can be verified.",
|
|
98
|
+
targetSources: [
|
|
99
|
+
"Selected or first rendered post from the SDK-backed feed list.",
|
|
100
|
+
"Thread/comment composer receives the parent post object or postId from its parent post card.",
|
|
101
|
+
"SDK seed post/comment flow; use returned post/comment objects during the same run.",
|
|
102
|
+
],
|
|
103
|
+
tenantData: ["A visible parent post exists; seed at least one comment when the smoke should prove thread rendering."],
|
|
104
|
+
consolePrep: ["Confirm comment permissions, moderation settings, and whether flagged/reviewed comments should be visible."],
|
|
105
|
+
noDataFallback: [
|
|
106
|
+
"Ask whether to create a dogfood parent post and comment through the SDK, then pass the returned post/comment object through app state.",
|
|
107
|
+
"If the feed is empty and seeding is declined, skip populated comment proof and state that no natural parent post existed.",
|
|
108
|
+
"Do not ask the user to paste a postId; the parent post must come from the rendered feed, route state, or the seed return value.",
|
|
109
|
+
],
|
|
110
|
+
evidenceToRecord: ["Parent post title/text excerpt, resolved postId, and rendered comment count."],
|
|
111
|
+
};
|
|
112
|
+
case "chat":
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
smokeExpectation: "populated",
|
|
116
|
+
smokeReason: "Chat is only proven when the current user can open a real channel and see/send messages.",
|
|
117
|
+
targetSources: [
|
|
118
|
+
"Current user's channel inbox/list query, selecting a rendered channel.",
|
|
119
|
+
"SDK create-channel flow from selected users/community; use the returned channel object immediately.",
|
|
120
|
+
"Host route or app state when the product already owns channel selection.",
|
|
121
|
+
],
|
|
122
|
+
tenantData: ["A target channel exists, the current user is a member, and seed messages/unread state exist when those states are in scope."],
|
|
123
|
+
consolePrep: ["Confirm channel type, membership rules, message permissions, moderation, and read/unread expectations."],
|
|
124
|
+
noDataFallback: [
|
|
125
|
+
"Ask whether to create a dogfood channel and seed at least one message through the SDK using selected dogfood users.",
|
|
126
|
+
"Use the returned channel object or inbox query result immediately; do not put channelId into config.",
|
|
127
|
+
"If channel creation is declined and the inbox is empty, record that only empty inbox resolution can be proven.",
|
|
128
|
+
],
|
|
129
|
+
evidenceToRecord: ["Selected channel display name/type, resolved channelId, membership state, and visible message count."],
|
|
130
|
+
};
|
|
131
|
+
case "profile":
|
|
132
|
+
return {
|
|
133
|
+
...base,
|
|
134
|
+
smokeExpectation: "populated",
|
|
135
|
+
smokeReason: "Profile/follow work should prove SDK-backed identity and relationship data, not static profile chrome.",
|
|
136
|
+
targetSources: [
|
|
137
|
+
"Current Social+ user from host auth/session/current profile.",
|
|
138
|
+
"Selected user from member, follower/following, chat participant, or search state.",
|
|
139
|
+
"SDK seed/search flow that creates or finds a dogfood peer user, then uses the returned user object.",
|
|
140
|
+
],
|
|
141
|
+
tenantData: ["The current user exists in Social+, and at least one other dogfood user exists for follow/follower state when in scope."],
|
|
142
|
+
consolePrep: ["Confirm user profile visibility, relationship/follow permissions, and any role/ban restrictions."],
|
|
143
|
+
noDataFallback: [
|
|
144
|
+
"Ask whether to create/find a safe dogfood peer user through SDK search/create flows.",
|
|
145
|
+
"If no peer exists and seeding is declined, prove only current-user profile resolution and mark relationship proof unavailable.",
|
|
146
|
+
"Do not ask the user to paste targetId; select the peer through app search, member lists, followers, chat participants, or seed return values.",
|
|
147
|
+
],
|
|
148
|
+
evidenceToRecord: ["Current user display name, selected peer display name, resolved user IDs, and relationship counts/states."],
|
|
149
|
+
};
|
|
150
|
+
case "notifications":
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
smokeExpectation: "resolved",
|
|
154
|
+
smokeReason: "Notification settings can be proven with readable settings; live push delivery needs separate device/provider setup.",
|
|
155
|
+
targetSources: [
|
|
156
|
+
"Current logged-in Social+ user and the platform notification permission state.",
|
|
157
|
+
"Push token/provider callback when live delivery is explicitly in scope.",
|
|
158
|
+
],
|
|
159
|
+
tenantData: ["A current Social+ user exists; push tokens/provider credentials are required only when push delivery is in scope."],
|
|
160
|
+
consolePrep: ["Confirm notification settings, push provider setup, token ownership, and logout cleanup expectations."],
|
|
161
|
+
noDataFallback: [
|
|
162
|
+
"If push/provider data is missing, ask whether runtime proof should stop at settings resolution or include provider setup.",
|
|
163
|
+
"Do not synthesize device tokens or provider credentials; use the platform callback and local-only config.",
|
|
164
|
+
],
|
|
165
|
+
evidenceToRecord: ["Notification settings read result, permission state, and token presence as set/missing only."],
|
|
166
|
+
};
|
|
167
|
+
default:
|
|
168
|
+
return {
|
|
169
|
+
...base,
|
|
170
|
+
smokeExpectation: "resolved",
|
|
171
|
+
smokeReason: "Empty state may be legitimate unless the selected product story requires seeded data.",
|
|
172
|
+
targetSources: [
|
|
173
|
+
"Host route/app state, SDK query result, or SDK create flow that returns the target object at runtime.",
|
|
174
|
+
],
|
|
175
|
+
tenantData: ["Identify the natural target source and minimum dogfood records this surface needs before runtime smoke."],
|
|
176
|
+
consolePrep: ["Confirm tenant permissions and moderation settings let the dogfood user exercise the selected surface."],
|
|
177
|
+
noDataFallback: [
|
|
178
|
+
"If discovery returns no data, ask whether to prepare Console data, seed safe SDK data, or downgrade to resolved empty-state proof.",
|
|
179
|
+
],
|
|
180
|
+
evidenceToRecord: ["Resolved target display label, target type, and target ID in smoke evidence only."],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function runtimeReadinessForStages(stages) {
|
|
185
|
+
return {
|
|
186
|
+
status: "needs-review",
|
|
187
|
+
placement: "before-dogfood",
|
|
188
|
+
summary: "Review identity, credential source, natural target discovery, tenant data, Console prep, runtime-proof consent, and populated-vs-resolved smoke expectations before feeding this workplan to a dogfood agent.",
|
|
189
|
+
identity: {
|
|
190
|
+
source: "host-auth-or-local-env",
|
|
191
|
+
blockingWhenUnknown: true,
|
|
192
|
+
localEnv: ["AMITY_USER_ID", "AMITY_DISPLAY_NAME"],
|
|
193
|
+
guidance: [
|
|
194
|
+
"Derive userId from the host app's auth/session/current profile whenever one exists.",
|
|
195
|
+
"For dogfood-only projects, use an explicit local env/config userId and label it as dogfood data.",
|
|
196
|
+
"Do not invent or hardcode userId, object IDs, reaction names, or region in source code.",
|
|
197
|
+
"Resolve community, feed, post, comment, channel, and target-user IDs from SDK queries, create flows, route state, or user selection.",
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
credentials: {
|
|
201
|
+
source: "local-only-env-or-host-config",
|
|
202
|
+
localEnv: ["AMITY_API_KEY", "AMITY_REGION"],
|
|
203
|
+
guidance: [
|
|
204
|
+
"Keep API keys, auth tokens, and provider credentials in ignored local env/config or existing host configuration.",
|
|
205
|
+
"Commit only templates/placeholders; never log or echo secret-shaped values.",
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
targetDiscovery: {
|
|
209
|
+
mode: "natural-runtime-discovery",
|
|
210
|
+
doNotRequire: ["communityId", "channelId", "targetId", "postId", "feedId", "commentId"],
|
|
211
|
+
order: [
|
|
212
|
+
"Prefer an existing host-app object: current auth/session user, route state, selected item, profile/member list, content detail, inbox, or composer context.",
|
|
213
|
+
"If the host app has no selected object, use SDK discovery/list/search queries for the current user and select a rendered result by display label.",
|
|
214
|
+
"Skip discovered objects whose display labels/descriptions are raw IDs, numeric-only labels, short random strings such as `rand2`, lorem/test data, `sampleapp` fixtures, or unrelated generated business copy; keep searching or create product-native seed data instead of rendering them.",
|
|
215
|
+
"Prefer a product-owned namespace/allowlist: app-created names, metadata tags, session/program identifiers, or cached IDs returned by the app's own create flow. If a broad SDK query returns mixed tenant data, render only product-safe matches and never let an unsafe object become the active selected target.",
|
|
216
|
+
"Deduplicate discovered/seeded targets by stable SDK ID or normalized product label, and prioritize populated product-owned targets before empty clones. A long rail of repeated zero-post targets is not polished populated proof.",
|
|
217
|
+
"Do not rewrite unsafe tenant labels/descriptions into product copy to make them look acceptable. Exclude those objects from every visible rail/list/card and author fallback.",
|
|
218
|
+
"Apply product-safe filtering to every visible rail/list/card and author fallback, not just the selected target. A hidden bad target is acceptable; a visible random tenant label is not populated product proof.",
|
|
219
|
+
"If discovery is empty, ask whether to prepare tenant data in Console or create safe dogfood seed data through SDK functions.",
|
|
220
|
+
"Use SDK-returned objects or subsequent query results in normal app state; do not copy object IDs into source, config, fixtures, or env files.",
|
|
221
|
+
],
|
|
222
|
+
noData: {
|
|
223
|
+
status: "prepare-or-seed-before-populated-proof",
|
|
224
|
+
options: [
|
|
225
|
+
"prepare data in Console and re-run discovery",
|
|
226
|
+
"create safe dogfood data through SDK functions in the current tenant, then use the returned object",
|
|
227
|
+
"decline data setup and run only resolved empty-state proof where that is a legitimate product state",
|
|
228
|
+
],
|
|
229
|
+
ifDeclined: "Tell the user that populated runtime proof cannot be claimed. Static Vise checks and build sensors may pass, but empty tenant data can still hide route, session, permission, and rendering gaps.",
|
|
230
|
+
},
|
|
231
|
+
evidencePolicy: [
|
|
232
|
+
"Record resolved IDs only in sp-vise evidence, screenshots/log summaries, or local dogfood reports.",
|
|
233
|
+
"Evidence should include human-readable labels, source path (host state, SDK query, or seed function), visible counts, and whether data was pre-existing or created for dogfood.",
|
|
234
|
+
"Visible labels in product UI should be product-relevant; raw object IDs, random strings, numeric-only names, and unrelated tenant fixtures belong only in evidence, not the default screen.",
|
|
235
|
+
"Runtime screenshots must be product-clean. Flutter/React Native overflow banners, red error boxes, clipped action rows, or `BOTTOM OVERFLOWED BY ... PIXELS` are runtime failures to fix before claiming proof.",
|
|
236
|
+
"For any surface whose smokeExpectation is populated, an empty state is a failed populated proof even when the empty state is well-designed; seed or prepare data before claiming the surface is product-proven.",
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
runtimeProof: {
|
|
240
|
+
consent: {
|
|
241
|
+
required: true,
|
|
242
|
+
question: "Runtime proof can boot browsers, simulators, emulators, or devices and consume local resources plus agent tokens. Ask the user before running it.",
|
|
243
|
+
yes: [
|
|
244
|
+
"Run the target app, capture screenshot/log evidence, and run `vise smoke` when a smoke contract exists.",
|
|
245
|
+
"Record natural target discovery or seed provenance, visible counts, platform, region, and runtime user display name.",
|
|
246
|
+
],
|
|
247
|
+
no: [
|
|
248
|
+
"Stop after static Vise checks and project sensors, then state that runtime behavior is unproven.",
|
|
249
|
+
"Explain the remaining risks: cold-launch session races, empty or mis-permissioned tenant data, route wiring gaps, and native packaging issues.",
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
cost: [
|
|
253
|
+
"May require browser automation, simulator/emulator boot, native build/install time, device permissions, network access, and additional agent context.",
|
|
254
|
+
"May require temporary tenant data creation if natural discovery returns empty results.",
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
surfaces: stages.map(surfaceReadiness),
|
|
258
|
+
nextStep: "Confirm this runtime readiness packet before spawning a dogfood agent. Ask for runtime-proof consent; if the user says yes, discover natural targets first, prepare Console/SDK seed data when discovery is empty, then run runtime smoke with the listed expectation and record resolved runtime targets as evidence. Do not accept a polished empty state as populated proof. If the user says no, explain the expected unproven runtime risks instead of claiming product proof.",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function renderList(items) {
|
|
262
|
+
return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
|
|
263
|
+
}
|
|
264
|
+
function renderRuntimeReadiness(readiness) {
|
|
265
|
+
const surfaceRows = readiness.surfaces
|
|
266
|
+
.map((surface) => `<div class="ready-surface">
|
|
267
|
+
<div class="ready-head"><span class="lbl">${escapeHtml(surface.label)}</span><span class="pill">${escapeHtml(surface.smokeExpectation)}</span></div>
|
|
268
|
+
<p class="why">${escapeHtml(surface.smokeReason)}</p>
|
|
269
|
+
<div class="cols"><div><b>target sources</b>${renderList(surface.targetSources)}</div><div><b>tenant data</b>${renderList(surface.tenantData)}</div></div>
|
|
270
|
+
<div class="cols"><div><b>console prep</b>${renderList(surface.consolePrep)}</div><div><b>no data fallback</b>${renderList(surface.noDataFallback)}</div></div>
|
|
271
|
+
<div><b>runtime evidence</b>${renderList(surface.evidenceToRecord)}<p class="seed">${escapeHtml(surface.seedGuidance)}</p></div>
|
|
272
|
+
</div>`)
|
|
273
|
+
.join("\n");
|
|
274
|
+
return `<div class="card"><p class="h">runtime readiness · before dogfood</p>
|
|
275
|
+
<p class="note">${escapeHtml(readiness.summary)}</p>
|
|
276
|
+
<div class="cols"><div><b>identity env</b>${renderList(readiness.identity.localEnv)}</div><div><b>credential env</b>${renderList(readiness.credentials.localEnv)}</div></div>
|
|
277
|
+
<div class="cols"><div><b>identity</b>${renderList(readiness.identity.guidance)}</div><div><b>credentials</b>${renderList(readiness.credentials.guidance)}</div></div>
|
|
278
|
+
<div class="cols"><div><b>natural target discovery</b>${renderList(readiness.targetDiscovery.order)}<p class="seed">Do not require ${escapeHtml(readiness.targetDiscovery.doNotRequire.join(", "))} as setup inputs.</p></div><div><b>if no data exists</b>${renderList(readiness.targetDiscovery.noData.options)}<p class="seed">${escapeHtml(readiness.targetDiscovery.noData.ifDeclined)}</p></div></div>
|
|
279
|
+
<div class="cols"><div><b>ask before runtime proof</b><p class="seed">${escapeHtml(readiness.runtimeProof.consent.question)}</p>${renderList(readiness.runtimeProof.consent.yes)}</div><div><b>if user declines proof</b>${renderList(readiness.runtimeProof.consent.no)}<p class="seed">${escapeHtml(readiness.runtimeProof.cost.join(" "))}</p></div></div>
|
|
280
|
+
${surfaceRows}
|
|
281
|
+
<p class="note">${escapeHtml(readiness.nextStep)}</p></div>`;
|
|
282
|
+
}
|
|
283
|
+
export function renderFlowBlueprint(input) {
|
|
284
|
+
const exp = input.experience;
|
|
285
|
+
const runtimeReadiness = input.runtimeReadiness ?? runtimeReadinessForStages(input.stages);
|
|
286
|
+
const stageRows = input.stages
|
|
287
|
+
.map((stage, index) => `<li><span class="n">${index + 1}</span><span class="lbl">${escapeHtml(stage.label ?? stage.id)}</span><span class="meta">${escapeHtml(stage.id)} · ${escapeHtml(stage.outcome)}</span></li>`)
|
|
288
|
+
.join("\n");
|
|
289
|
+
const omitted = input.omitted ?? [];
|
|
290
|
+
const pathSummary = input.pathDecision ? "choose sdk / uikit / hybrid" : input.path ?? "undecided";
|
|
291
|
+
const experienceSummary = exp?.selected ? `${exp.selected.title} (${exp.selected.id})` : "no EI selection provided";
|
|
292
|
+
const surfaceSummary = `${input.stages.length} active surface${input.stages.length === 1 ? "" : "s"}`;
|
|
293
|
+
const designSummary = input.design ? `contract ${input.design}` : "no accepted design contract";
|
|
294
|
+
const trimSummary = omitted.length ? `${omitted.length} surface${omitted.length === 1 ? "" : "s"} trimmed` : "none";
|
|
295
|
+
const summaryCard = `
|
|
296
|
+
<div class="card priority"><p class="h">approval checklist · confirm these before build</p>
|
|
297
|
+
<div class="row"><b>path</b><span>${escapeHtml(pathSummary)}</span></div>
|
|
298
|
+
<div class="row"><b>experience</b><span>${escapeHtml(experienceSummary)}</span></div>
|
|
299
|
+
<div class="row"><b>surfaces</b><span>${escapeHtml(surfaceSummary)}</span></div>
|
|
300
|
+
<div class="row"><b>design</b><span>${escapeHtml(designSummary)}</span></div>
|
|
301
|
+
<div class="row"><b>trimmed</b><span>${escapeHtml(trimSummary)}</span></div>
|
|
302
|
+
</div>`;
|
|
303
|
+
const omittedCard = omitted.length
|
|
304
|
+
? `
|
|
305
|
+
<div class="card warning"><p class="h">out of scope · deliberately omitted, not built</p><ol>
|
|
306
|
+
${omitted
|
|
307
|
+
.map((o) => `<li><span class="lbl" style="text-decoration:line-through;opacity:.7">${escapeHtml(o.label ?? o.id)}</span><span class="meta">${escapeHtml(o.id)} · ${escapeHtml(o.outcome)}</span></li><li style="border-top:0;padding-top:0"><span class="row" style="opacity:.7">${escapeHtml(o.reason)}</span></li>`)
|
|
308
|
+
.join("\n")}
|
|
309
|
+
</ol></div>`
|
|
310
|
+
: "";
|
|
311
|
+
const pathCard = input.pathDecision
|
|
312
|
+
? `
|
|
313
|
+
<div class="card"><p class="h">1 · solution path · choose one</p><ol>
|
|
314
|
+
${input.pathDecision.options
|
|
315
|
+
.map((opt) => `<li><span class="lbl">${escapeHtml(opt)}</span></li>`)
|
|
316
|
+
.join("\n")}
|
|
317
|
+
</ol><p class="note">${escapeHtml(input.pathDecision.question)} The agent records your pick with <code>--answer solution_path=<sdk|uikit|hybrid></code>, then re-confirms.</p></div>`
|
|
318
|
+
: `
|
|
319
|
+
<div class="card"><p class="h">1 · solution path</p><div class="row"><b>path</b><span>${escapeHtml(input.path ?? "undecided")}</span></div></div>`;
|
|
320
|
+
const experienceCard = exp && exp.selected
|
|
321
|
+
? `
|
|
322
|
+
<div class="card"><p class="h">2 · EI experience · confirm or switch</p><ol>
|
|
323
|
+
<li><span class="lbl">${escapeHtml(exp.selected.title)}</span><span class="meta">recommended · ${escapeHtml(exp.selected.id)}</span></li>
|
|
324
|
+
${exp.alternatives
|
|
325
|
+
.map((a) => `<li><span class="lbl" style="opacity:.75">${escapeHtml(a.title)}</span><span class="meta">${escapeHtml(a.id)}</span></li>`)
|
|
326
|
+
.join("\n")}
|
|
327
|
+
</ol>${exp.alternatives.length ? `<p class="note">To build a different experience, re-run <code>vise creative accept . --variant <id> --rationale "<why>" --confidence high</code> and re-confirm the updated blueprint.</p>` : ""}</div>`
|
|
328
|
+
: "";
|
|
329
|
+
return `<!DOCTYPE html>
|
|
330
|
+
<html lang="en"><head><meta charset="utf-8" />
|
|
331
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
332
|
+
<title>Vise — build blueprint</title>
|
|
333
|
+
<style>
|
|
334
|
+
:root{color-scheme:light dark}
|
|
335
|
+
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.5;padding:28px 20px 48px;max-width:740px;margin:0 auto}
|
|
336
|
+
h1{font-size:20px;font-weight:500;margin:0 0 12px}
|
|
337
|
+
.kicker{font-size:12px;opacity:.6;margin:0 0 8px}
|
|
338
|
+
.req{font-size:14px;opacity:.85;background:rgba(127,127,127,.12);padding:11px 13px;border-radius:8px;margin:0 0 18px}
|
|
339
|
+
.card{border:0.5px solid rgba(127,127,127,.35);border-radius:12px;padding:14px 16px;margin:0 0 12px}
|
|
340
|
+
.card.priority{border-color:rgba(32,128,96,.55);background:rgba(32,128,96,.08)}
|
|
341
|
+
.card.warning{border-color:rgba(180,120,20,.55);background:rgba(180,120,20,.08)}
|
|
342
|
+
.h{font-size:13px;opacity:.65;margin:0 0 10px}
|
|
343
|
+
ol{list-style:none;margin:0;padding:0}
|
|
344
|
+
ol>li{display:flex;align-items:baseline;gap:10px;padding:7px 0;border-top:0.5px solid rgba(127,127,127,.2)}
|
|
345
|
+
ol>li:first-child{border-top:0}
|
|
346
|
+
.n{font-size:12px;opacity:.6;min-width:16px}
|
|
347
|
+
.lbl{font-weight:500;font-size:14px}
|
|
348
|
+
.meta{font-size:12px;opacity:.55;margin-left:auto;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
349
|
+
.row{display:flex;gap:8px;font-size:13px;opacity:.85;margin:4px 0}
|
|
350
|
+
.row b{font-weight:500;opacity:.6;min-width:70px}
|
|
351
|
+
.note,.why,.seed{font-size:13px;opacity:.78;margin:0 0 10px}
|
|
352
|
+
.note{margin:10px 0 0}
|
|
353
|
+
.seed{margin:8px 0 0}
|
|
354
|
+
.cols{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
355
|
+
.cols b{display:block;font-size:12px;font-weight:500;opacity:.6;margin:0 0 5px}
|
|
356
|
+
.cols ul{margin:0;padding-left:18px}
|
|
357
|
+
.cols li{line-height:1.45}
|
|
358
|
+
.ready-surface{border-top:0.5px solid rgba(127,127,127,.2);padding:10px 0 0;margin:10px 0 0}
|
|
359
|
+
.ready-head{display:flex;align-items:center;gap:8px;justify-content:space-between;margin:0 0 6px}
|
|
360
|
+
.pill{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;padding:2px 6px;border-radius:999px;background:rgba(127,127,127,.14);opacity:.75}
|
|
361
|
+
.digest{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;opacity:.6;margin-top:16px}
|
|
362
|
+
code{font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
363
|
+
@media (max-width:640px){.cols{grid-template-columns:1fr}.meta{display:none}}
|
|
364
|
+
</style></head><body>
|
|
365
|
+
<p class="kicker">vise · build blueprint · sp-vise/flow-blueprint.html</p>
|
|
366
|
+
<h1>Review and confirm before build</h1>
|
|
367
|
+
<div class="req">${escapeHtml(input.request)}</div>${summaryCard}${pathCard}${experienceCard}
|
|
368
|
+
<div class="card"><p class="h">3 · surfaces in scope · built and verified in order</p><ol>
|
|
369
|
+
${stageRows}
|
|
370
|
+
</ol></div>${omittedCard}${renderRuntimeReadiness(runtimeReadiness)}
|
|
371
|
+
<div class="card priority"><p class="h">sign-off · blocked until approved</p>
|
|
372
|
+
<p style="font-size:13.5px;opacity:.85;margin:0">${input.pathDecision ? `Choose the solution path first (the agent re-runs with <code>--answer solution_path=<choice></code>); the journey below is built and verified in order on the chosen path. ` : ""}Build cannot start until this blueprint is signed off. After review, the agent records your approval with <code>--answer blueprint_confirmation=${escapeHtml(input.digest)}</code>. You'll only be asked again if the path, journey, or design source changes.</p>
|
|
373
|
+
<p class="digest">blueprint digest ${escapeHtml(input.digest)}</p></div>
|
|
374
|
+
</body></html>
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
export async function writeFlowBlueprintHtml(repoRoot, html) {
|
|
378
|
+
const target = flowBlueprintPath(repoRoot);
|
|
379
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
380
|
+
await writeFile(target, html, "utf8");
|
|
381
|
+
return target;
|
|
382
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const STATUS_GLOSS = {
|
|
2
|
+
green: "All applicable rules pass.",
|
|
3
|
+
"needs-attestation": "Some rules need a host-agent or human attestation before they can pass.",
|
|
4
|
+
"deterministic-failures": "A deterministic sensor found a violation in the source.",
|
|
5
|
+
blocked: "An external prerequisite the customer must provide is missing.",
|
|
6
|
+
"contract-drift": "The recorded ruleset no longer matches the installed Vise — re-run init.",
|
|
7
|
+
"completeness-gap": "Required capabilities are neither built nor opted out (scope-omit) yet.",
|
|
8
|
+
"selected-capability-failures": "A selected optional capability's sensor failed.",
|
|
9
|
+
"needs-clarification": "Blocking intake questions are unresolved.",
|
|
10
|
+
};
|
|
11
|
+
function asString(value) {
|
|
12
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function asArray(value) {
|
|
15
|
+
return Array.isArray(value) ? value : [];
|
|
16
|
+
}
|
|
17
|
+
function bullet(lines, indent = " ") {
|
|
18
|
+
return lines.map((line) => `${indent}- ${line}`).join("\n");
|
|
19
|
+
}
|
|
20
|
+
function renderCompliance(payload) {
|
|
21
|
+
const status = asString(payload.status) ?? "unknown";
|
|
22
|
+
const exitCode = typeof payload.exitCode === "number" ? payload.exitCode : undefined;
|
|
23
|
+
const outcome = asString(payload.outcome);
|
|
24
|
+
const surface = asString(payload.surfacePath);
|
|
25
|
+
const out = [];
|
|
26
|
+
const head = `social.plus compliance: ${status.toUpperCase()}${exitCode !== undefined ? ` (exit ${exitCode})` : ""}`;
|
|
27
|
+
const scope = [outcome ? `outcome: ${outcome}` : null, surface ? `surface: ${surface}` : null].filter(Boolean).join(", ");
|
|
28
|
+
out.push(scope ? `${head} — ${scope}` : head);
|
|
29
|
+
if (STATUS_GLOSS[status])
|
|
30
|
+
out.push(STATUS_GLOSS[status]);
|
|
31
|
+
const summary = payload.summary;
|
|
32
|
+
if (summary && Object.keys(summary).length > 0) {
|
|
33
|
+
const parts = Object.entries(summary).map(([key, count]) => `${count} ${key}`);
|
|
34
|
+
out.push("", `Rules: ${parts.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
const basisNote = asString(payload.evidence_basis_note);
|
|
37
|
+
if (basisNote)
|
|
38
|
+
out.push(`Evidence: ${basisNote}`);
|
|
39
|
+
const blocking = asArray(payload.rules)
|
|
40
|
+
.filter((r) => typeof r === "object" && r !== null)
|
|
41
|
+
.filter((r) => {
|
|
42
|
+
const s = asString(r.status);
|
|
43
|
+
return s !== undefined && s !== "deterministic-pass" && s !== "attested" && s !== "advisory";
|
|
44
|
+
});
|
|
45
|
+
if (blocking.length > 0) {
|
|
46
|
+
out.push("", "Needs action:");
|
|
47
|
+
out.push(bullet(blocking.slice(0, 20).map((r) => {
|
|
48
|
+
const id = asString(r.ruleId) ?? asString(r.contractRuleId) ?? "(rule)";
|
|
49
|
+
const reason = asString(r.reason) ?? asString(r.status) ?? "";
|
|
50
|
+
return reason ? `${id} [${asString(r.status)}]: ${reason}` : `${id} [${asString(r.status)}]`;
|
|
51
|
+
})));
|
|
52
|
+
if (blocking.length > 20)
|
|
53
|
+
out.push(` …and ${blocking.length - 20} more`);
|
|
54
|
+
}
|
|
55
|
+
const completeness = payload.completeness;
|
|
56
|
+
const missing = asArray(completeness?.missing).filter((m) => typeof m === "object" && m !== null);
|
|
57
|
+
if (missing.length > 0) {
|
|
58
|
+
out.push("", "Missing capabilities (build, or `// vise: scope-omit <id> — <reason>`):");
|
|
59
|
+
out.push(bullet(missing.map((m) => `${asString(m.id) ?? "(capability)"}: ${asString(m.hint) ?? asString(m.label) ?? ""}`.trim())));
|
|
60
|
+
}
|
|
61
|
+
const next = asString(payload.nextStep);
|
|
62
|
+
if (next)
|
|
63
|
+
out.push("", `Next: ${next}`);
|
|
64
|
+
return out.join("\n");
|
|
65
|
+
}
|
|
66
|
+
function renderPlan(payload) {
|
|
67
|
+
const out = [];
|
|
68
|
+
const outcome = asString(payload.outcome) ?? "(unrouted)";
|
|
69
|
+
const platform = asString(payload.platform);
|
|
70
|
+
out.push(`Plan: ${outcome}${platform ? ` — ${platform}` : ""}`);
|
|
71
|
+
const sp = payload.solutionPath;
|
|
72
|
+
if (sp) {
|
|
73
|
+
const rec = asString(sp.recommendation);
|
|
74
|
+
const conf = asString(sp.confidence);
|
|
75
|
+
const summary = asString(sp.summary);
|
|
76
|
+
out.push(`Solution path: ${rec ?? "(n/a)"}${conf ? ` (${conf})` : ""}${summary ? ` — ${summary}` : ""}`);
|
|
77
|
+
}
|
|
78
|
+
const uikit = payload.uikitCustomization;
|
|
79
|
+
if (uikit) {
|
|
80
|
+
out.push(`UIKit customization: ${asString(uikit.recommendedLevel) ?? "(n/a)"} (${asString(uikit.status) ?? "n/a"})`);
|
|
81
|
+
}
|
|
82
|
+
const decisions = asArray(payload.decisionsRequired);
|
|
83
|
+
if (decisions.length > 0) {
|
|
84
|
+
out.push("", "Decisions required:");
|
|
85
|
+
out.push(bullet(decisions.map((d) => (typeof d === "string" ? d : asString(d.question) ?? JSON.stringify(d)))));
|
|
86
|
+
}
|
|
87
|
+
const steps = asArray(payload.implementationSteps);
|
|
88
|
+
if (steps.length > 0) {
|
|
89
|
+
out.push("", "Build steps:");
|
|
90
|
+
out.push(steps
|
|
91
|
+
.slice(0, 30)
|
|
92
|
+
.map((s, i) => ` ${i + 1}. ${typeof s === "string" ? s : asString(s.step) ?? asString(s.description) ?? JSON.stringify(s)}`)
|
|
93
|
+
.join("\n"));
|
|
94
|
+
}
|
|
95
|
+
const inputs = asArray(payload.requiredInputs).filter((i) => typeof i === "string");
|
|
96
|
+
if (inputs.length > 0) {
|
|
97
|
+
out.push("", "Required inputs:");
|
|
98
|
+
out.push(bullet(inputs));
|
|
99
|
+
}
|
|
100
|
+
const engagement = payload.engagement;
|
|
101
|
+
if (engagement) {
|
|
102
|
+
const baseline = asArray(engagement.baselineLevers).filter((l) => typeof l === "string");
|
|
103
|
+
const optional = asArray(engagement.optionalLevers).filter((l) => typeof l === "string");
|
|
104
|
+
if (baseline.length > 0 || optional.length > 0) {
|
|
105
|
+
out.push("", "Engagement levers (advisory):");
|
|
106
|
+
if (baseline.length > 0)
|
|
107
|
+
out.push(` baseline: ${baseline.join(", ")}`);
|
|
108
|
+
if (optional.length > 0)
|
|
109
|
+
out.push(` optional: ${optional.join(", ")}`);
|
|
110
|
+
const deepen = asString(engagement.deepenWith);
|
|
111
|
+
if (deepen)
|
|
112
|
+
out.push(` ${deepen}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const intake = payload.intake;
|
|
116
|
+
const questions = asArray(intake?.questions).filter((q) => typeof q === "object" && q !== null);
|
|
117
|
+
const blockingQs = questions.filter((q) => q.blocksImplementationWhenMissing === true);
|
|
118
|
+
if (blockingQs.length > 0) {
|
|
119
|
+
out.push("", "Open intake (blocking):");
|
|
120
|
+
out.push(bullet(blockingQs.map((q) => `${asString(q.id) ?? "?"}: ${asString(q.prompt) ?? asString(q.question) ?? ""}`.trim())));
|
|
121
|
+
}
|
|
122
|
+
const next = asString(payload.nextStep);
|
|
123
|
+
if (next)
|
|
124
|
+
out.push("", `Next: ${next}`);
|
|
125
|
+
return out.join("\n");
|
|
126
|
+
}
|
|
127
|
+
function renderDoctor(payload) {
|
|
128
|
+
const out = [];
|
|
129
|
+
out.push(`${asString(payload.package) ?? "vise"} ${asString(payload.version) ?? ""} — ${asString(payload.status) ?? "?"}`.trim());
|
|
130
|
+
const buildSource = asString(payload.buildSource);
|
|
131
|
+
if (buildSource === "local") {
|
|
132
|
+
out.push(`⚠ build: LOCAL source build (npm link / dev checkout) — NOT the published npm package`);
|
|
133
|
+
const root = asString(payload.packageRoot);
|
|
134
|
+
if (root)
|
|
135
|
+
out.push(` root: ${root}`);
|
|
136
|
+
}
|
|
137
|
+
else if (buildSource === "published") {
|
|
138
|
+
out.push(`build: published npm package`);
|
|
139
|
+
}
|
|
140
|
+
if (payload.node)
|
|
141
|
+
out.push(`node: ${asString(payload.node)} (requires ${asString(payload.requiredNodeMajor) ?? "?"})`);
|
|
142
|
+
out.push(`docs: ${asString(payload.docsSource) ?? "?"}${payload.docsRoot ? ` (${asString(payload.docsRoot)})` : ""}`);
|
|
143
|
+
if (payload.transport)
|
|
144
|
+
out.push(`transport: ${asString(payload.transport)}`);
|
|
145
|
+
const tools = asArray(payload.tools);
|
|
146
|
+
if (tools.length > 0)
|
|
147
|
+
out.push(`tools: ${tools.length} registered`);
|
|
148
|
+
const support = asString(payload.support);
|
|
149
|
+
if (support)
|
|
150
|
+
out.push(`support: ${support}`);
|
|
151
|
+
return out.join("\n");
|
|
152
|
+
}
|
|
153
|
+
function renderExplain(payload) {
|
|
154
|
+
if (payload.kind === "rule-index") {
|
|
155
|
+
const out = [`${payload.count} compliance rules. Run \`vise explain <id>\` for the full rule.`, ""];
|
|
156
|
+
for (const entry of asArray(payload.rules).filter((e) => typeof e === "object" && e !== null)) {
|
|
157
|
+
const id = asString(entry.id) ?? "(rule)";
|
|
158
|
+
const sev = asString(entry.severity);
|
|
159
|
+
const outcomes = asArray(entry.outcomes).join(", ");
|
|
160
|
+
out.push(` ${id}${sev ? ` (${sev})` : ""}${outcomes ? ` — ${outcomes}` : ""}`);
|
|
161
|
+
}
|
|
162
|
+
return out.join("\n");
|
|
163
|
+
}
|
|
164
|
+
if (payload.kind === "product_expectation") {
|
|
165
|
+
const out = [`${asString(payload.id) ?? "(rule)"} — validated by ${asArray(payload.contract_rules).length} contract rule(s):`];
|
|
166
|
+
for (const c of asArray(payload.contract_rules).filter((e) => typeof e === "object" && e !== null)) {
|
|
167
|
+
out.push(` ${asString(c.contract_rule_id) ?? "?"} — ${asString(c.title) ?? ""}`.trimEnd());
|
|
168
|
+
}
|
|
169
|
+
return out.join("\n");
|
|
170
|
+
}
|
|
171
|
+
const out = [];
|
|
172
|
+
const id = asString(payload.id) ?? "(rule)";
|
|
173
|
+
const contractId = asString(payload.contract_rule_id);
|
|
174
|
+
const title = asString(payload.title);
|
|
175
|
+
const sev = asString(payload.severity);
|
|
176
|
+
const version = payload.version;
|
|
177
|
+
out.push(`${id}${contractId ? ` (${contractId})` : ""}${title ? ` — ${title}` : ""}${sev ? ` [${sev}${version !== undefined ? `, v${version}` : ""}]` : ""}`);
|
|
178
|
+
const rationale = asString(payload.rationale);
|
|
179
|
+
if (rationale)
|
|
180
|
+
out.push(rationale);
|
|
181
|
+
const applies = payload.applies_when;
|
|
182
|
+
if (applies) {
|
|
183
|
+
const o = asArray(applies.outcomes).join(", ");
|
|
184
|
+
const p = asArray(applies.platforms).join(", ");
|
|
185
|
+
const parts = [o ? `outcomes: ${o}` : null, p ? `platforms: ${p}` : null].filter(Boolean).join("; ");
|
|
186
|
+
if (parts)
|
|
187
|
+
out.push(`Applies — ${parts}`);
|
|
188
|
+
}
|
|
189
|
+
const feedforward = asString(payload.feedforward);
|
|
190
|
+
if (feedforward)
|
|
191
|
+
out.push(`Feedforward: ${feedforward}`);
|
|
192
|
+
const symptoms = asArray(payload.symptoms).filter((s) => typeof s === "string");
|
|
193
|
+
if (symptoms.length > 0)
|
|
194
|
+
out.push(`Symptoms: ${symptoms.join(" | ")}`);
|
|
195
|
+
const attestation = payload.attestation;
|
|
196
|
+
if (attestation)
|
|
197
|
+
out.push(`Attestation: ${attestation.allowed ? "allowed" : "not allowed"}${attestation.host_agent_min_confidence ? ` (host-agent min ${asString(attestation.host_agent_min_confidence)})` : ""}`);
|
|
198
|
+
const digest = asString(payload.rule_digest);
|
|
199
|
+
if (digest)
|
|
200
|
+
out.push(`digest: ${digest}`);
|
|
201
|
+
return out.join("\n");
|
|
202
|
+
}
|
|
203
|
+
function renderExplore(payload) {
|
|
204
|
+
const out = [];
|
|
205
|
+
const request = asString(payload.request) ?? "";
|
|
206
|
+
const matched = asString(payload.matched_outcome);
|
|
207
|
+
out.push(matched
|
|
208
|
+
? `social.plus — best match for "${request}": ${matched}`
|
|
209
|
+
: `social.plus — what you can build (no single match for "${request}"):`);
|
|
210
|
+
for (const route of asArray(payload.routes).filter((r) => typeof r === "object" && r !== null)) {
|
|
211
|
+
out.push("", `▸ ${asString(route.outcome) ?? "(outcome)"} — ${asString(route.summary) ?? ""}`.trimEnd());
|
|
212
|
+
const caps = asArray(route.capabilities).filter((c) => typeof c === "object" && c !== null);
|
|
213
|
+
if (caps.length > 0)
|
|
214
|
+
out.push(` capabilities: ${caps.map((c) => asString(c.id)).filter(Boolean).join(", ")}`);
|
|
215
|
+
const start = asString(route.start);
|
|
216
|
+
if (start)
|
|
217
|
+
out.push(` start: ${start}`);
|
|
218
|
+
}
|
|
219
|
+
const also = asArray(payload.also_available).filter((a) => typeof a === "object" && a !== null);
|
|
220
|
+
if (also.length > 0) {
|
|
221
|
+
out.push("", "Also available:");
|
|
222
|
+
out.push(bullet(also.map((a) => `${asString(a.outcome) ?? "?"} — ${asString(a.summary) ?? ""}`.trimEnd())));
|
|
223
|
+
}
|
|
224
|
+
const note = asString(payload.note);
|
|
225
|
+
if (note)
|
|
226
|
+
out.push("", note);
|
|
227
|
+
return out.join("\n");
|
|
228
|
+
}
|
|
229
|
+
const RENDERERS = {
|
|
230
|
+
check: renderCompliance,
|
|
231
|
+
status: renderCompliance,
|
|
232
|
+
plan: renderPlan,
|
|
233
|
+
doctor: renderDoctor,
|
|
234
|
+
explain: renderExplain,
|
|
235
|
+
explore: renderExplore,
|
|
236
|
+
};
|
|
237
|
+
export function wantsHumanFormat(format) {
|
|
238
|
+
return format === "human";
|
|
239
|
+
}
|
|
240
|
+
export function renderHuman(command, payload) {
|
|
241
|
+
const renderer = RENDERERS[command];
|
|
242
|
+
if (!renderer || typeof payload !== "object" || payload === null) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return renderer(payload);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|