@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 CHANGED
@@ -4,6 +4,36 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
4
4
 
5
5
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.3.0 — 2026-06-19
8
+
9
+ Discovery + readability surface, CLI/report honesty, and a batch of detector-precision fixes from two real-agent persona-dogfood sweeps (Sonnet 4.6), each fix mutation-verified and the whole batch adversarially reviewed. **No change to `vise check` exit codes, compliance rules, or sidecar *formats*** — the sidecar gains only **additive** fields (see Compatibility). The rule corpus is unchanged, so the contract digest is stable and no re-attestation is triggered.
10
+
11
+ ### Added
12
+ - **`vise explore "<request>"`** — a pre-credentials, read-only discovery command. It maps a free-text request to a candidate feature/setup outcome (and lists the full menu when it can't, instead of dead-ending on `outcome:unknown`); each route carries the capabilities involved, the canonical docs, and the `vise plan` command to start. No project or API key required.
13
+ - **`--format human`** on the read commands (`check` · `status` · `plan` · `doctor` · `explain` · `explore`). **JSON stays the default** — agents, CI, and sidecar-consuming flows parse stdout, so the default is unchanged; `--format human` is opt-in.
14
+ - **`vise explain`** with no id now **enumerates the valid rule ids** (grouped by public id → contract ids) instead of erroring.
15
+ - **`completeness` / selected-capability detail** is now surfaced by **`vise status`** and the init-time `findings.json` snapshot, mirroring `vise check`. Previously a `needs-attestation` / `deterministic-failures` headline (status precedence) hid a co-existing completeness-gap from anything reading those two surfaces.
16
+ - **Brownfield baseline gate.** `vise baseline` (and `vise init --baseline`) snapshots the current pre-existing findings to `sp-vise/baseline.json`, and **`vise check --new-only`** (also `status --new-only`) gates only on findings introduced *since* the baseline — legacy findings are reported (`baselined: true`) but excluded. This makes `check` usable as a per-PR merge gate on an existing codebase. **Gate integrity:** subtraction is an explicit per-invocation flag, *never* the default — a bare `vise check` always gates on everything, so a green exit can never silently mean "no *new* gaps." A stale baseline (ruleset digest moved since it was recorded) is **not** applied (fail-safe: gate on everything and disclose `baseline.stale`). The artifact is additive — absent ⇒ unchanged behavior; the frozen sidecar-compat baseline is unaffected. Known v1 limitation, surfaced in the check output as `baseline.residual_caveat`: the rule+file key can mask a new violation of an already-baselined rule in the same file.
17
+
18
+ ### Changed
19
+ - **Honesty of reports.** `attest` re-runs the deterministic sensor and **discloses** a live violation (without refusing — attestation is the override path); `check`/`status` carry an `evidence_basis_note` (plain-language "passed by absence" caveat); `vise status` propagates the body exit code to the process exit; `--ci` `blockingResultStatuses` enumerates every non-green status (incl. completeness-gap, selected-capability-failures); `explain` surfaces the feedforward/symptoms/inferential corpus fields; `doctor` surfaces the support channel.
20
+ - **`vise debug`** correlates runtime-only symptom matches regardless of compliance status (a passing rule is framed as a runtime/out-of-sensor-reach signal, never a compliance failure), and maps `Cannot find module` / `MODULE_NOT_FOUND` for a **bare package** to a concrete `npm install` remediation — a failing correlated rule still wins the headline (the module hint rides as a secondary note).
21
+ - **`vise init` intake validation.** Out-of-enum values (on closed-enum questions) and unrecognized `--answer` keys now produce advisory **warnings** instead of being silently accepted/dropped. Warnings only — never a rejection — so multi-select and free-form answers are unaffected.
22
+ - **`engagement --scope`** accepts the full planner outcome vocabulary (derived from the classifier order), including `add-follow` / `add-community` / `add-notifications`, which were previously rejected.
23
+ - **Classifier routing.** "community profile" → `add-community`, "user/member profile" (with a social qualifier) → `add-follow`, "notifications tray" → `add-notifications`; a non-social "company/performance/memory profile" stays `unknown`.
24
+ - **`design check` coverage** splits `referenced_in_app` vs `seeded_only_tokens` — tokens that appear only in the Vise-scaffolded token file no longer inflate a false 100% (the raw `referenced` count is unchanged for back-compat).
25
+
26
+ ### Fixed
27
+ - **Advisory routing precision** (advisory-only; no gate change): an explicit "build it with our own component" / "replace the UIKit surface" ask now routes SDK/custom-UI instead of Dynamic-UI; Console/theming phrasing corrected.
28
+ - **Detector false positives.** `posts.status-filtered` no longer mis-attributes to `.d.ts` type stubs; `chat.send-error-handling` requires a real message *send* call (a read-only `markRead` view no longer fires); Android `comments.observer-cleanup` recognizes structured-concurrency cleanup (`stateIn(viewModelScope)` / `collectAsStateWithLifecycle` / `repeatOnLifecycle` / `DisposableEffect`), anchored to the observer's own chain; a negated "no custom code" no longer trips behavior-overrides; `scope-omit` reasons must be auditable (≥ 8 chars).
29
+ - **`*.sdk.version.pinned` messages** now name each platform's real uncontrolled tokens (Android `+`/`:latest`, Flutter `any`, iOS moving branch / unspecified CocoaPods, TypeScript `latest`/`*`/`x` with `^`/`~` accepted). The rule **rationale is unchanged** (it is part of the contract digest; rewording it would force re-attestation — see the digest note in maintainer docs).
30
+ - **`sdk-facts`**: the "names-only grounding" caveat now prints only when grounding is actually names-only; native platforms note that an empty `capabilities` list reflects TypeScript-first capability authoring, not lack of SDK support.
31
+ - **Design contract** confirmation re-routes to `needs-confirmation` when the preview is absent; the design reference inlines only sanitized `:root` token declarations, never raw source.
32
+ - Concrete CLI bugs from the first sweep (broken docs path, `engagement` show/init path mismatch, `workplan.json` persistence, and related discovery/intake edges).
33
+
34
+ ### Compatibility
35
+ - **No exit-code or sidecar-*format* breaking changes.** The `completeness` / `selectedOptionalCapabilities` fields added to `vise status` / the init `findings.json` snapshot, and the new `sp-vise/baseline.json` artifact, are all **additive**; the `test:sidecar-compat` baseline (frozen 1.1.0 sidecar, no baseline.json) stays green. The rule corpus and contract digest are unchanged, so existing attestations are not invalidated. `vise check` with no flags is unchanged — the brownfield baseline only applies under the explicit `--new-only` flag.
36
+
7
37
  ## 1.2.0 — 2026-06-18
8
38
 
9
39
  Adds advisory **solution-path / UIKit routing** and a fail-closed fix to the SwiftPM manifest sensor. No change to `vise check` exit codes, compliance rules, or sidecar **formats**; the one behavior change is the SwiftPM-timeout outcome noted under Changed.
package/README.md CHANGED
@@ -169,12 +169,13 @@ Each platform has dozens of rules across 10 compliance domains (feed, comments,
169
169
 
170
170
  ## CLI Reference
171
171
 
172
- Run `vise <command> --help` for full flags. JSON output is the default for agent-facing commands.
172
+ Run `vise <command> --help` for full flags. JSON output is the default for agent-facing commands; the read commands (`check`, `status`, `plan`, `doctor`, `explain`, `explore`) also accept `--format human` for a readable summary.
173
173
 
174
174
  ### Inspect, plan, initialize
175
175
 
176
176
  | Command | Purpose |
177
177
  |---|---|
178
+ | `vise explore "<request>"` | Pre-credentials discovery: map a request to what social.plus offers (candidate outcome, capabilities, docs, next command). No project or API key needed |
178
179
  | `vise doctor` | Verify install; print version, install path, docs source |
179
180
  | `vise inspect [path]` | Detect platform, monorepo surfaces, design signals, available sensors |
180
181
  | `vise plan [path] --request "..." [--summary]` | Grounded implementation plan with intake questions and docs citations; `--summary` prints a compact route/intake view |
@@ -225,6 +226,8 @@ Everything in this group is local and advisory: no uploads, no `vise check` exit
225
226
  |---|---|
226
227
  | `vise check [path]` | Re-validate against the recorded contract: `green`, `needs-attestation`, `deterministic-failures`, `blocked`, or `contract-drift` |
227
228
  | `vise check [path] --ci` | Read-only variant that exits non-zero unless green (for CI) |
229
+ | `vise check [path] --new-only` | **Brownfield gate.** With a recorded baseline, gate only on findings introduced *since* the baseline (pre-existing ones are reported but excluded). Default `check` always gates on everything |
230
+ | `vise baseline [path]` | Snapshot the current pre-existing findings to `sp-vise/baseline.json` so `check --new-only` can separate legacy debt from new gaps. `vise init --baseline` records it at init time |
228
231
  | `vise validate [path]` | Run the deterministic validators only (no attestation comparison) |
229
232
  | `vise sync [path]` | Persist deterministic-pass evidence to `sp-vise/attestations/` |
230
233
  | `vise attest [path] --rule <id> --signer host-agent --confidence high --evidence-file evidence.json --rationale "..."` | Record an attestation when a rule passes through architecture the deterministic check can't see |
@@ -1256,6 +1256,7 @@ export function assessSelectedOptionalCapabilities(source, outcome, selectedIds
1256
1256
  note: "Selected optional capabilities are enforced only after the user or host agent opts in through `feed_optional_capabilities`. They are source sensors, not baseline compliance rules.",
1257
1257
  };
1258
1258
  }
1259
+ const MIN_SCOPE_OMIT_REASON_CHARS = 8;
1259
1260
  export function assessCompleteness(source, outcome) {
1260
1261
  const caps = baselineCapabilities(outcome);
1261
1262
  const optOuts = new Map();
@@ -1265,12 +1266,14 @@ export function assessCompleteness(source, outcome) {
1265
1266
  while ((match = omitPattern.exec(source)) !== null) {
1266
1267
  const id = match[1].toLowerCase();
1267
1268
  const reason = (match[2] ?? "").trim();
1268
- if (reason) {
1269
+ if (reason.length >= MIN_SCOPE_OMIT_REASON_CHARS) {
1269
1270
  optOuts.set(id, reason);
1270
1271
  invalidOptOuts.delete(id);
1271
1272
  }
1272
1273
  else if (!optOuts.has(id)) {
1273
- invalidOptOuts.set(id, "scope-omit marker must include a reason");
1274
+ invalidOptOuts.set(id, reason.length === 0
1275
+ ? "scope-omit marker must include a reason"
1276
+ : `scope-omit reason is too short ("${reason}") — give an auditable reason of at least ${MIN_SCOPE_OMIT_REASON_CHARS} characters`);
1274
1277
  }
1275
1278
  }
1276
1279
  const present = [];
@@ -1292,7 +1295,7 @@ export function assessCompleteness(source, outcome) {
1292
1295
  missing.push({
1293
1296
  id: cap.id,
1294
1297
  label: cap.label,
1295
- hint: invalidReason ? `${cap.hint}; found scope-omit marker without a reason, so add a reason or implement it` : cap.hint,
1298
+ hint: invalidReason ? `${cap.hint}; ${invalidReason}` : cap.hint,
1296
1299
  });
1297
1300
  }
1298
1301
  }
@@ -0,0 +1,51 @@
1
+ import { capabilityChecklist } from "./capabilities.js";
2
+ import { classifyOutcome, getOutcomeDefinition } from "./outcomes.js";
3
+ const DISCOVERY_MENU = [
4
+ "setup-sdk",
5
+ "add-feed",
6
+ "add-comments",
7
+ "add-chat",
8
+ "add-community",
9
+ "add-follow",
10
+ "add-moderation",
11
+ "add-notifications",
12
+ "setup-push",
13
+ "setup-live-data",
14
+ ];
15
+ function outcomeBrief(outcome, platform, request) {
16
+ const def = getOutcomeDefinition(outcome);
17
+ return {
18
+ outcome,
19
+ summary: def.interpretation,
20
+ capabilities: capabilityChecklist(outcome).map((capability) => ({ id: capability.id, label: capability.label })),
21
+ docs: def.docs(platform).slice(0, 4).map((doc) => ({ path: doc.path, reason: doc.reason })),
22
+ start: `vise plan . --request ${JSON.stringify(request)}`,
23
+ };
24
+ }
25
+ export function exploreRequest(request, platform = "typescript") {
26
+ const matched = classifyOutcome(request);
27
+ const isKnown = matched !== "unknown" && DISCOVERY_MENU.includes(matched);
28
+ if (isKnown) {
29
+ return {
30
+ kind: "exploration",
31
+ request,
32
+ platform,
33
+ matched_outcome: matched,
34
+ routes: [outcomeBrief(matched, platform, request)],
35
+ also_available: DISCOVERY_MENU.filter((outcome) => outcome !== matched).map((outcome) => ({
36
+ outcome,
37
+ summary: getOutcomeDefinition(outcome).interpretation,
38
+ })),
39
+ note: "Discovery is pre-credentials and read-only — no project or API key needed. When you're ready to build, `vise plan`/`vise init` starts a tracked integration.",
40
+ };
41
+ }
42
+ return {
43
+ kind: "exploration",
44
+ request,
45
+ platform,
46
+ matched_outcome: null,
47
+ unmatched_note: "This request didn't map to a single social.plus outcome. Below is everything Vise can guide you through — pick one and run its `start` command.",
48
+ routes: DISCOVERY_MENU.map((outcome) => outcomeBrief(outcome, platform, request)),
49
+ note: "Discovery is pre-credentials and read-only — no project or API key needed. When you're ready to build, `vise plan`/`vise init` starts a tracked integration.",
50
+ };
51
+ }
@@ -0,0 +1,226 @@
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 intake = payload.intake;
101
+ const questions = asArray(intake?.questions).filter((q) => typeof q === "object" && q !== null);
102
+ const blockingQs = questions.filter((q) => q.blocksImplementationWhenMissing === true);
103
+ if (blockingQs.length > 0) {
104
+ out.push("", "Open intake (blocking):");
105
+ out.push(bullet(blockingQs.map((q) => `${asString(q.id) ?? "?"}: ${asString(q.prompt) ?? asString(q.question) ?? ""}`.trim())));
106
+ }
107
+ const next = asString(payload.nextStep);
108
+ if (next)
109
+ out.push("", `Next: ${next}`);
110
+ return out.join("\n");
111
+ }
112
+ function renderDoctor(payload) {
113
+ const out = [];
114
+ out.push(`${asString(payload.package) ?? "vise"} ${asString(payload.version) ?? ""} — ${asString(payload.status) ?? "?"}`.trim());
115
+ if (payload.node)
116
+ out.push(`node: ${asString(payload.node)} (requires ${asString(payload.requiredNodeMajor) ?? "?"})`);
117
+ out.push(`docs: ${asString(payload.docsSource) ?? "?"}${payload.docsRoot ? ` (${asString(payload.docsRoot)})` : ""}`);
118
+ if (payload.transport)
119
+ out.push(`transport: ${asString(payload.transport)}`);
120
+ const tools = asArray(payload.tools);
121
+ if (tools.length > 0)
122
+ out.push(`tools: ${tools.length} registered`);
123
+ const support = asString(payload.support);
124
+ if (support)
125
+ out.push(`support: ${support}`);
126
+ return out.join("\n");
127
+ }
128
+ function renderExplain(payload) {
129
+ if (payload.kind === "rule-index") {
130
+ const out = [`${payload.count} compliance rules. Run \`vise explain <id>\` for the full rule.`, ""];
131
+ for (const entry of asArray(payload.rules).filter((e) => typeof e === "object" && e !== null)) {
132
+ const id = asString(entry.id) ?? "(rule)";
133
+ const sev = asString(entry.severity);
134
+ const outcomes = asArray(entry.outcomes).join(", ");
135
+ out.push(` ${id}${sev ? ` (${sev})` : ""}${outcomes ? ` — ${outcomes}` : ""}`);
136
+ }
137
+ return out.join("\n");
138
+ }
139
+ if (payload.kind === "product_expectation") {
140
+ const out = [`${asString(payload.id) ?? "(rule)"} — validated by ${asArray(payload.contract_rules).length} contract rule(s):`];
141
+ for (const c of asArray(payload.contract_rules).filter((e) => typeof e === "object" && e !== null)) {
142
+ out.push(` ${asString(c.contract_rule_id) ?? "?"} — ${asString(c.title) ?? ""}`.trimEnd());
143
+ }
144
+ return out.join("\n");
145
+ }
146
+ const out = [];
147
+ const id = asString(payload.id) ?? "(rule)";
148
+ const contractId = asString(payload.contract_rule_id);
149
+ const title = asString(payload.title);
150
+ const sev = asString(payload.severity);
151
+ const version = payload.version;
152
+ out.push(`${id}${contractId ? ` (${contractId})` : ""}${title ? ` — ${title}` : ""}${sev ? ` [${sev}${version !== undefined ? `, v${version}` : ""}]` : ""}`);
153
+ const rationale = asString(payload.rationale);
154
+ if (rationale)
155
+ out.push(rationale);
156
+ const applies = payload.applies_when;
157
+ if (applies) {
158
+ const o = asArray(applies.outcomes).join(", ");
159
+ const p = asArray(applies.platforms).join(", ");
160
+ const parts = [o ? `outcomes: ${o}` : null, p ? `platforms: ${p}` : null].filter(Boolean).join("; ");
161
+ if (parts)
162
+ out.push(`Applies — ${parts}`);
163
+ }
164
+ const feedforward = asString(payload.feedforward);
165
+ if (feedforward)
166
+ out.push(`Feedforward: ${feedforward}`);
167
+ const symptoms = asArray(payload.symptoms).filter((s) => typeof s === "string");
168
+ if (symptoms.length > 0)
169
+ out.push(`Symptoms: ${symptoms.join(" | ")}`);
170
+ const attestation = payload.attestation;
171
+ if (attestation)
172
+ out.push(`Attestation: ${attestation.allowed ? "allowed" : "not allowed"}${attestation.host_agent_min_confidence ? ` (host-agent min ${asString(attestation.host_agent_min_confidence)})` : ""}`);
173
+ const digest = asString(payload.rule_digest);
174
+ if (digest)
175
+ out.push(`digest: ${digest}`);
176
+ return out.join("\n");
177
+ }
178
+ function renderExplore(payload) {
179
+ const out = [];
180
+ const request = asString(payload.request) ?? "";
181
+ const matched = asString(payload.matched_outcome);
182
+ out.push(matched
183
+ ? `social.plus — best match for "${request}": ${matched}`
184
+ : `social.plus — what you can build (no single match for "${request}"):`);
185
+ for (const route of asArray(payload.routes).filter((r) => typeof r === "object" && r !== null)) {
186
+ out.push("", `▸ ${asString(route.outcome) ?? "(outcome)"} — ${asString(route.summary) ?? ""}`.trimEnd());
187
+ const caps = asArray(route.capabilities).filter((c) => typeof c === "object" && c !== null);
188
+ if (caps.length > 0)
189
+ out.push(` capabilities: ${caps.map((c) => asString(c.id)).filter(Boolean).join(", ")}`);
190
+ const start = asString(route.start);
191
+ if (start)
192
+ out.push(` start: ${start}`);
193
+ }
194
+ const also = asArray(payload.also_available).filter((a) => typeof a === "object" && a !== null);
195
+ if (also.length > 0) {
196
+ out.push("", "Also available:");
197
+ out.push(bullet(also.map((a) => `${asString(a.outcome) ?? "?"} — ${asString(a.summary) ?? ""}`.trimEnd())));
198
+ }
199
+ const note = asString(payload.note);
200
+ if (note)
201
+ out.push("", note);
202
+ return out.join("\n");
203
+ }
204
+ const RENDERERS = {
205
+ check: renderCompliance,
206
+ status: renderCompliance,
207
+ plan: renderPlan,
208
+ doctor: renderDoctor,
209
+ explain: renderExplain,
210
+ explore: renderExplore,
211
+ };
212
+ export function wantsHumanFormat(format) {
213
+ return format === "human";
214
+ }
215
+ export function renderHuman(command, payload) {
216
+ const renderer = RENDERERS[command];
217
+ if (!renderer || typeof payload !== "object" || payload === null) {
218
+ return null;
219
+ }
220
+ try {
221
+ return renderer(payload);
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ }
package/dist/outcomes.js CHANGED
@@ -42,20 +42,21 @@ const CHAT_PATTERNS = [
42
42
  ];
43
43
  const COMMUNITY_PATTERNS = [
44
44
  /\b(create|creating|build|building|manage|managing|join|joining|leave|leaving|set ?up)\s+(a\s+|the\s+)?communit/,
45
- /\bcommunit\w*\s+(member|membership|role|invit|categor|setting|management|directory|discovery)/,
45
+ /\bcommunit\w*\s+(member|membership|role|invit|categor|setting|management|directory|discovery|profile|page)/,
46
46
  /\bcommunity\s+(creation|management|members?|roles?|invitations?|categories|settings|moderation)\b/,
47
47
  ];
48
48
  const FOLLOW_PATTERNS = [
49
49
  /\b(follow|unfollow|follower|followers|following)\b/,
50
50
  /\b(social graph|user relationship|follow request|followers? list|following list)\b/,
51
+ /\b(?:user'?s?|member|account|my|your|their)\s+profile\b|\buser_profile_page\b/,
51
52
  /\bblocked\s+users?\b/,
52
53
  /\bmanage\s+blocked\b/,
53
54
  /\bblock\s*list\b/,
54
55
  /\bunblock\b/,
55
56
  ];
56
57
  const NOTIFICATION_PATTERNS = [
57
- /\b(notification tray|notification cent(?:er|re)|in-?app notification)/,
58
- /\bnotification (?:setting|preference)/,
58
+ /\b(notifications?\s+tray|notifications?\s+cent(?:er|re)|in-?app notifications?)/,
59
+ /\bnotifications?\s+(?:setting|preference)/,
59
60
  ];
60
61
  const TROUBLESHOOT_PATTERNS = [/\b(error|broken|crash|not working|fail|timeout|401|403)\b/];
61
62
  const VALIDATE_PATTERNS = [/\b(validate|check|correct|setup right|initiali[sz])\b/];
@@ -212,7 +213,7 @@ export function liveDataPlatformPath(platform) {
212
213
  const setupSdk = {
213
214
  id: "setup-sdk",
214
215
  patterns: SETUP_PATTERNS,
215
- interpretation: "Implement setup-sdk.",
216
+ interpretation: "Set up the social.plus SDK — client creation with the correct region/endpoint, secure session login, and access-token renewal — the foundation every other social.plus feature builds on.",
216
217
  docsQuery: (platform) => `${platform} quick start setup`,
217
218
  docs: (platform) => [
218
219
  platformQuickStart(platform),
@@ -489,8 +490,8 @@ const addFeed = {
489
490
  docsQuery: (platform) => `${platform} social feed posts`,
490
491
  docs: (platform) => [
491
492
  {
492
- path: "social-plus-sdk/social/posts",
493
- reason: "Canonical social post/feed concepts and API entrypoint.",
493
+ path: "social-plus-sdk/social/content-management/posts/creation/text-post",
494
+ reason: "Canonical post model and creation API entrypoint; pair with the live-objects/collections docs below for feed querying.",
494
495
  },
495
496
  {
496
497
  path: "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview",
@@ -601,7 +602,7 @@ const addFeed = {
601
602
  {
602
603
  step: "Fetch the canonical social/feed docs and use platform-appropriate live collection patterns.",
603
604
  evidence: [
604
- "social-plus-sdk/social/posts",
605
+ "social-plus-sdk/social/content-management/posts/creation/text-post",
605
606
  "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview",
606
607
  ],
607
608
  },
@@ -633,11 +634,11 @@ const addFeed = {
633
634
  { step: "Implement loading, empty, error, and data states.", evidence: ["implementationRules.file-specific edits"] },
634
635
  {
635
636
  step: "In post card renderers, resolve each media type from the parent post OR its childrenPosts — in a feed the parent is usually dataType 'text' and the image/video/poll/clip/room rides on a child post. Handle at minimum: text, image, video, file, poll, clip, room. Do not render placeholder labels like '[Image post]' or '[Poll post]'; read the SDK data/accessors and render the actual content. Do NOT gate media on the parent's dataType alone (e.g. post.dataType === 'poll') — that never matches a text parent and the content silently never renders. When rendering the text body itself (post or comment), apply @mention highlights if metadata carries mention entries ({ type: 'user', index, length, userId }, length excluding the '@'): wrap each [index, index + length + 1] span in a styled element and resolve userId to a display name, rather than printing raw text. Pass mentionees on create so mentioned users are notified.",
636
- evidence: ["social-plus-sdk/social/posts", "intake.feed_post_type_scope", "capabilityAvailability.available"],
637
+ evidence: ["social-plus-sdk/social/content-management/posts/creation/text-post", "intake.feed_post_type_scope", "capabilityAvailability.available"],
637
638
  },
638
639
  {
639
640
  step: "Read SDK objects through their own typed accessors and types (e.g. post.getImageInfo(), the Amity.* types) instead of casting return values to hand-written shapes like (post.data as { text?: string }). A hand-written cast silences the type-checker, so when an SDK upgrade renames a field your build still compiles and the bug only surfaces at runtime — reading through SDK types lets tsc/analyze flag the breakage.",
640
- evidence: ["social-plus-sdk/social/posts"],
641
+ evidence: ["social-plus-sdk/social/content-management/posts/creation/text-post"],
641
642
  },
642
643
  {
643
644
  step: "For community and user avatars, use the SDK-provided URL (community.avatar?.fileUrl / community.avatarImage?.getUrl / community.getAvatar()?.getUrl) and fall back to an initial only when the field is absent. Do not derive an initial from the display name as the sole identifier.",
@@ -671,12 +672,12 @@ const addFeed = {
671
672
  step: "If the post composer supports poll creation, implement the two-step creation chain: (1) create the poll with the platform SDK poll repository — returns a Poll/pollId; (2) link that poll into a post with the platform post builder, for example `PostRepository.createPost({ targetType, targetId, data: { text: '', pollId } })` on TypeScript/React Native or Android's dedicated `createPollPost(targetType, targetId, pollId, text, ...)` API. Rendering poll answers (votePoll/unvotePoll) is the read-side; without both creation steps the poll composer silently does nothing. Offer a dedicated poll-builder UI (question input + dynamic answer list) so users can author polls inline.",
672
673
  evidence: [
673
674
  "social-plus-sdk/social/content-management/posts/creation/poll-post",
674
- "social-plus-sdk/social/posts",
675
+ "social-plus-sdk/social/content-management/posts/creation/text-post",
675
676
  ],
676
677
  },
677
678
  {
678
679
  step: "In post card headers, show the post's target context when post.targetType === 'community': display 'Author.displayName › Community.displayName' by subscribing to CommunityRepository.getCommunity(post.targetId, cb) for the live community name.",
679
- evidence: ["social-plus-sdk/social/communities", "social-plus-sdk/social/posts"],
680
+ evidence: ["social-plus-sdk/social/communities", "social-plus-sdk/social/content-management/posts/creation/text-post"],
680
681
  },
681
682
  {
682
683
  step: "For room-type posts, subscribe to RoomRepository.getRoom(roomId, cb) to get the Room object and display room.title (not room.roomId). Show room.status (live/idle/ended) as a badge.",
@@ -684,7 +685,7 @@ const addFeed = {
684
685
  },
685
686
  {
686
687
  step: "Give each post card an actions menu gated by the viewer's relationship to the post. If the viewer is the author (post.postedUserId === currentUserId), show edit (PostRepository.editPost) and delete (PostRepository.deletePost). For posts the viewer does not own, show report/flag (PostRepository.flagPost). Author-ownership actions and moderator-role actions are distinct — do not show edit/delete to non-authors.",
687
- evidence: ["social-plus-sdk/social/posts", "social-plus-sdk/social/moderation"],
688
+ evidence: ["social-plus-sdk/social/content-management/posts/creation/text-post", "social-plus-sdk/social/moderation"],
688
689
  },
689
690
  { step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
690
691
  ];
@@ -891,7 +892,7 @@ const addModeration = {
891
892
  const questions = [
892
893
  {
893
894
  id: "moderation_target",
894
- question: "What content types need moderation (post, comment, message, user)?",
895
+ question: "What content types need moderation (posts, comments, messages, users, or all)?",
895
896
  why: "Each content type has different moderation APIs and UI patterns.",
896
897
  required: true,
897
898
  blocksImplementationWhenMissing: true,
@@ -922,7 +923,7 @@ const addModeration = {
922
923
  return filterAnswered(ctx.answers, questions);
923
924
  },
924
925
  requiredInputs: () => [
925
- "moderation target types: post, comment, message, user",
926
+ "moderation target types: posts, comments, messages, users, or all",
926
927
  "moderation action set: report, block, mute, hide, admin queue",
927
928
  "post-action rendering policy for blocked/hidden content",
928
929
  ],