@amityco/social-plus-vise 1.1.1 → 1.2.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 +13 -0
- package/README.md +18 -9
- package/dist/server.js +174 -8
- package/dist/solutionPath.js +274 -0
- package/dist/tools/harness.js +30 -2
- package/dist/tools/integration.js +161 -10
- package/dist/tools/project.js +13 -5
- package/dist/tools/sensors.js +1 -1
- package/dist/uikitCustomization.js +370 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.2.0 — 2026-06-18
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Solution-path & UIKit routing (advisory).** `vise plan` emits `solutionPath` (`sdk` · `uikit` · `hybrid` · `needs-decision`) so an agent pauses for `--answer solution_path=…` before building the wrong layer, cleanly separating the two integrator postures (SDK/custom-UI vs UIKit/prebuilt-UI). When UIKit is in play it also emits `uikitCustomization`, recommending the lowest-effort tier that meets the goal (Dynamic UI → Component Styling → Localization → Behavior Overrides → Fork and Extend → SDK Custom UI). Both are **advisory only** — they never change outcome classification, compliance rules, the sidecar schema, or `vise check` exit codes. Documented under "Solution path & UIKit routing" in the README; the two postures are described in `docs/PERSONAS.md`.
|
|
13
|
+
- **Real-build readiness checks** surfaced alongside the routing signals.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **SwiftPM manifest sensor: timeout is now RED (fail-closed), not a skip.** A manifest-parse timeout previously read as a clean green; for a pure-SwiftPM iOS project the manifest sensor is the only sensor, so a stall masqueraded as success. It now reports `timed-out` and the CLI exits non-zero. **Behavior note for consumers:** a network-restricted CI runner that cannot resolve a remote SwiftPM dependency will now go RED on this sensor rather than silently passing — intended fail-closed behavior, not a regression.
|
|
17
|
+
- **Advisory routing engines hardened (two adversarial-verification batches, 28 fixes, every fix mutation-verified).** `recommendSolutionPath` / `recommendUIKitCustomization` precision: UIKit-rejection coverage now spans modal/contracted negators (won't/can't/cannot), intervening verbs ("don't want to use UIKit", "without using UIKit"), and "anything but UIKit" — an explicit rejection routes SDK-first instead of (previously) recommending UIKit; prebuilt idioms are anchored to a UI/social noun; `off-the-shelf` is anchored (so "off-the-shelf analytics" stays SDK-first); localization no longer fires on a programming/design "language"; backend feature capabilities (e.g. livestreaming) surface as a decision rather than a Dynamic-UI default. Advisory-only quality improvements; no gate behavior changes.
|
|
18
|
+
- **Personas split into two integrator postures** (AI-Assisted SDK/Custom-UI Integrator and AI-Assisted UIKit/Prebuilt-UI Adopter), tied to a deterministic behavior contract in the test suite.
|
|
19
|
+
|
|
7
20
|
## 1.1.1 — 2026-06-16
|
|
8
21
|
|
|
9
22
|
Docs-only patch; no CLI, MCP, rule, or sidecar behavior changes.
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ It turns the request into a grounded plan, records a local contract under `sp-vi
|
|
|
47
47
|
|
|
48
48
|
> 🔒 **Your source code never leaves your machine.** Vise fetches only the public social.plus docs and the SDK's published version on npm — never your code, file contents, or search queries. `VISE_DOCS_OFFLINE=1` runs fully offline.
|
|
49
49
|
|
|
50
|
-
Vise can also run **ahead** of that loop: an advisory [Engagement Intelligence](#engagement-intelligence) layer turns a product goal into
|
|
50
|
+
Vise can also run **ahead** of that loop: an advisory [Engagement Intelligence](#engagement-intelligence) layer turns a product goal into multiple candidate engagement strategies — *what you might build*, not only *whether you built it right*.
|
|
51
51
|
|
|
52
52
|
> **Why "Vise"?** A bench vise holds the workpiece steady so the craftsman's hands are free to shape it. Vise clamps the integration to the real docs, the real project structure, and the real compliance rules so the agent can focus on building instead of guessing.
|
|
53
53
|
|
|
@@ -98,12 +98,18 @@ Prefer a per-project install? `npm install -D @amityco/social-plus-vise`, then c
|
|
|
98
98
|
## How it works
|
|
99
99
|
|
|
100
100
|
1. **Inspect** — `vise inspect` detects the platform, app surfaces, available sensors, and design signals from the local repo.
|
|
101
|
-
2. **Plan** — `vise plan --request "..."` classifies the outcome, cites docs, and raises blocking intake and design-preview questions. The agent surfaces
|
|
101
|
+
2. **Plan** — `vise plan --request "..."` classifies the outcome, cites docs, and raises blocking intake and design-preview questions. Use `--summary` when you only need the route/intake readout before implementation. The agent surfaces questions; you answer.
|
|
102
102
|
3. **Initialize** — `vise init` writes the `sp-vise/` compliance contract. While blocking questions are unanswered it refuses, returns `status: "needs-clarification"`, and exits 7 — the agent must ask instead of guessing.
|
|
103
103
|
4. **Build** — the agent edits your code, grounded by `vise search-docs` and `vise get-doc-page`.
|
|
104
104
|
5. **Check & repair** — `vise check` reports deterministic findings, completeness gaps, and attestation needs. The agent fixes findings or records attestations with evidence, looping until green.
|
|
105
105
|
6. **Sense** — `vise run-sensors` runs your project's own typecheck/build/lint/SDK smokes. Done means the contract and evidence are committed, not just that the agent stopped.
|
|
106
106
|
|
|
107
|
+
### Solution path & UIKit routing (advisory)
|
|
108
|
+
|
|
109
|
+
Some requests are better served by **social.plus UIKit** (prebuilt social surfaces) than by hand-rolling standard UI directly from SDK primitives. `vise plan` emits an advisory **`solutionPath`** — `sdk`, `uikit`, `hybrid`, or `needs-decision` — so an agent can pause for `--answer solution_path=uikit|sdk|hybrid` before it builds the wrong layer. This separates the two integrator postures cleanly: an **SDK / custom-UI** build (differentiated, direct-SDK) stays SDK-first, a **UIKit / prebuilt-UI** adoption (standard surfaces, fast launch) routes to UIKit, and a genuinely mixed request resolves to `needs-decision` rather than silently picking one. A request that *rejects* UIKit ("don't use UIKit", "we can't use UIKit") routes SDK-first, and a request that names a backend feature capability (e.g. livestreaming) is surfaced as a decision rather than a UIKit-customization default.
|
|
110
|
+
|
|
111
|
+
When UIKit is in play, `vise plan` can also emit **`uikitCustomization`**, recommending the lowest-effort customization tier that meets the goal — Dynamic UI → Component Styling → Localization → Behavior Overrides → Fork and Extend → SDK Custom UI. Both signals are **advisory only**: they never change outcome classification, compliance rules, the sidecar schema, or `vise check` exit codes.
|
|
112
|
+
|
|
107
113
|
### Three validation layers
|
|
108
114
|
|
|
109
115
|
The layer is set by the *kind of claim*, which is how Vise is designed to avoid false positives where it gates:
|
|
@@ -120,9 +126,9 @@ Correctness is gated by deterministic rules or attestations; completeness is gat
|
|
|
120
126
|
|
|
121
127
|
### Engagement Intelligence
|
|
122
128
|
|
|
123
|
-
**Advisory, in preview.** The three layers above answer *whether you built it right*. Ahead of the build, Vise can also help
|
|
129
|
+
**Advisory, in preview.** The three layers above answer *whether you built it right*. Ahead of the build, Vise can also help reason about *what to build*: an **Engagement Intelligence** layer turns a product goal into multiple candidate **engagement strategies** — archetypes, UX patterns, and solution variants drawn from a social.plus experience catalog — with rationale, tradeoffs, no-fit guidance, availability boundaries, and review gaps. The human or driving agent still chooses the direction, then Vise compiles that selected variant into an implementation plan (`vise creative` → `vise creative accept` → `vise experience compile`), optionally bridging to installable social.plus blocks, plus advisory UX expectations and a multi-dimension experience review.
|
|
124
130
|
|
|
125
|
-
It is **local-only, never uploads, carries no calibrated score
|
|
131
|
+
It is **local-only, never uploads, carries no calibrated score**, and **never changes `vise check`'s exit codes**. The opt-in ranking preview is review context, not a top-1 confidence claim or autonomous product-strategy selector. Use it to shape the work; the validation layers still decide when it's done.
|
|
126
132
|
|
|
127
133
|
### Design contracts
|
|
128
134
|
|
|
@@ -143,6 +149,8 @@ The mechanism is the durable claim: a checked loop beats the same rules in the p
|
|
|
143
149
|
|
|
144
150
|
**Feed completeness (capabilities selected):** when the optional capabilities — image, poll, edit — are selected up front, the Vise arm completed **97–100%** of an 11-item feed checklist across three agents (Cursor/Composer, Claude/Sonnet, Codex/GPT). This is *absolute* completeness from answering Vise's capability questions — **not** a lift over a baseline.
|
|
145
151
|
|
|
152
|
+
**Engagement Intelligence advisory quality:** a 24-case held-out dogfood set supports the advisory positioning: expected available strategies surfaced in **17/17** cases, multiple strategy options were visible in **20/20** strategy-offer cases, rationale/tradeoff evidence was visible in **24/24**, and no-fit plus availability-gated cases preserved their boundaries. The same run retained **4 ranking-calibration concerns**, so this is **not** a claim of top-1 ranking confidence, autonomous strategy selection, business-outcome lift, or a calibrated score.
|
|
153
|
+
|
|
146
154
|
**Boundaries:** these are social.plus's own measurements on greenfield work — self-reported, no third-party audit, measured on earlier builds, and not universal claims. Negative results travel with them: no measured advantage on day-2 bug fixing, and the design-conformance arms were measured at different times (the figure is the robust by-name-token signal, not pixel perfection).
|
|
147
155
|
|
|
148
156
|
<sub>Cursor, Claude, Codex, GitHub Copilot, VS Code, and other product names are trademarks of their respective owners; social.plus is not affiliated with or endorsed by them. Benchmark figures are from social.plus's own measurements.</sub>
|
|
@@ -155,7 +163,7 @@ The mechanism is the durable claim: a checked loop beats the same rules in the p
|
|
|
155
163
|
| **React Native** | ✅ Full | `tsc`, `npm lint`, SDK import smoke |
|
|
156
164
|
| **Flutter / Dart** | ✅ Full | `flutter analyze`, `flutter test` |
|
|
157
165
|
| **Android (Kotlin)** | ✅ Full | Gradle assemble, unit tests |
|
|
158
|
-
| **iOS (Swift)** | ✅ Full | Static rules fully operational (tree-sitter AST for highest-risk rules);
|
|
166
|
+
| **iOS (Swift)** | ✅ Full | Static rules fully operational (tree-sitter AST for highest-risk rules); `Package.swift` enables a SwiftPM manifest sensor, and `.xcodeproj`/`.xcworkspace` enables a guarded `xcodebuild` sensor when available |
|
|
159
167
|
|
|
160
168
|
Each platform has dozens of rules across 10 compliance domains (feed, comments, moderation, chat, secrets, session & auth, notifications, live objects, logging hygiene, design tokens).
|
|
161
169
|
|
|
@@ -169,7 +177,8 @@ Run `vise <command> --help` for full flags. JSON output is the default for agent
|
|
|
169
177
|
|---|---|
|
|
170
178
|
| `vise doctor` | Verify install; print version, install path, docs source |
|
|
171
179
|
| `vise inspect [path]` | Detect platform, monorepo surfaces, design signals, available sensors |
|
|
172
|
-
| `vise plan [path] --request "..."` | Grounded implementation plan with intake questions and docs citations |
|
|
180
|
+
| `vise plan [path] --request "..." [--summary]` | Grounded implementation plan with intake questions and docs citations; `--summary` prints a compact route/intake view |
|
|
181
|
+
| `vise plan --summary "..."` | Shortcut for quick routing dogfood or discovery when the current directory is the repo |
|
|
173
182
|
| `vise plan-harness [path] --request "..."` | Pre-planning step: build the harness around the request |
|
|
174
183
|
| `vise init [path] --request "..." [--answer key=value]` | Write the `sp-vise/` compliance contract once blocking intake is answered; exits 7 (`needs-clarification`) otherwise |
|
|
175
184
|
| `vise workplan next [path] --request "..."` | For broad social requests: print the next uncompleted surface and its focused commands |
|
|
@@ -180,7 +189,7 @@ Run `vise <command> --help` for full flags. JSON output is the default for agent
|
|
|
180
189
|
|
|
181
190
|
| Command | Purpose |
|
|
182
191
|
|---|---|
|
|
183
|
-
| `vise creative [path] --request "..." [--requirements <path\|none>] [--prototype <html>] [--ranking-preview]` | Write an advisory Engagement Intelligence brief with candidate solution variants; `--ranking-preview` adds
|
|
192
|
+
| `vise creative [path] --request "..." [--requirements <path\|none>] [--prototype <html>] [--ranking-preview]` | Write an advisory Engagement Intelligence brief with multiple candidate solution variants, rationale, tradeoffs, and review gaps; `--ranking-preview` adds opt-in, local-only review context that never reorders the default candidates |
|
|
184
193
|
| `vise creative accept [path] --variant <id>` | Record the selected variant so `plan`/`init`/`workplan` carry it forward; `--variant none --rationale "..."` records a catalog-gap signal instead |
|
|
185
194
|
| `vise ux-harness [path]` | Generate advisory UX expectations from the accepted selection |
|
|
186
195
|
| `vise experience compile [path]` | Compile the accepted variant into an implementation artifact plan |
|
|
@@ -189,7 +198,7 @@ Run `vise <command> --help` for full flags. JSON output is the default for agent
|
|
|
189
198
|
| `vise learning record [path]` | Append a local-only learning event; refreshes the learning summary |
|
|
190
199
|
| `vise learning show [path]` | Read the local learning summary (never changes recommendations) |
|
|
191
200
|
|
|
192
|
-
Everything in this group is local and advisory: no uploads, no `vise check` exit-code changes, no auto-accepted variants,
|
|
201
|
+
Everything in this group is local and advisory: no uploads, no `vise check` exit-code changes, no auto-accepted variants, no top-1 confidence claim, and no calibrated score.
|
|
193
202
|
|
|
194
203
|
### Design contract
|
|
195
204
|
|
|
@@ -299,7 +308,7 @@ Vise writes local planning, compliance, design, and evidence artifacts under `sp
|
|
|
299
308
|
| `sp-vise/inspection.json` | `vise init` | Platform, surface, and design signals detected at init |
|
|
300
309
|
| `sp-vise/attestations/*.json` | `vise sync` / `vise attest` | Per-rule evidence: signer, confidence, rationale, source fingerprints for drift detection |
|
|
301
310
|
| `sp-vise/creative-brief.json` + `creative-brief.md` | `vise creative` | Advisory brief: goals, archetypes, candidate variants (JSON + human-readable) |
|
|
302
|
-
| `sp-vise/candidate-ranking-preview.json` | `vise creative --ranking-preview` | Opt-in local ranking preview; `experience_score: null`, no uploads, no default-order change |
|
|
311
|
+
| `sp-vise/candidate-ranking-preview.json` | `vise creative --ranking-preview` | Opt-in local ranking preview for review context; `experience_score: null`, no uploads, no default-order change, no top-1 confidence claim |
|
|
303
312
|
| `sp-vise/creative-selection.json` | `vise creative accept` | Accepted variant and plan/workplan feed-forward context |
|
|
304
313
|
| `sp-vise/catalog-gap.json` | `vise creative accept --variant none` | Local-only no-fit signal for human catalog review |
|
|
305
314
|
| `sp-vise/ux-harness.json` | `vise creative accept` or `vise ux-harness` | Advisory UX pattern expectations and anti-patterns |
|
package/dist/server.js
CHANGED
|
@@ -285,12 +285,16 @@ async function handleCli(args) {
|
|
|
285
285
|
return "exit";
|
|
286
286
|
}
|
|
287
287
|
if (command === "plan" || command === "plan-integration") {
|
|
288
|
-
await
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
const input = await planCliInput(args);
|
|
289
|
+
if (hasFlag(args, "summary")) {
|
|
290
|
+
const result = await planIntegrationTool.call(input);
|
|
291
|
+
const text = result.content.map((item) => item.text).join("\n");
|
|
292
|
+
const payload = JSON.parse(text);
|
|
293
|
+
console.log(JSON.stringify(planSummary(payload), null, 2));
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
await printToolResult(planIntegrationTool, input);
|
|
297
|
+
}
|
|
294
298
|
return "exit";
|
|
295
299
|
}
|
|
296
300
|
if (command === "plan-harness") {
|
|
@@ -583,7 +587,9 @@ Create the evidence-backed implementation packet before editing code.
|
|
|
583
587
|
|
|
584
588
|
Usage:
|
|
585
589
|
vise plan [repoPath] --request "Add a social feed"
|
|
590
|
+
vise plan --summary "Use UIKit for a standard feed"
|
|
586
591
|
vise plan apps/web --request "Create posts" --surface apps/web
|
|
592
|
+
vise plan . --request "Use UIKit for a standard feed" --summary
|
|
587
593
|
|
|
588
594
|
Re-plan with collected answers (repeat --answer for each intake question):
|
|
589
595
|
vise plan . --request "Add a social feed" \\
|
|
@@ -993,6 +999,33 @@ async function printToolResult(tool, input) {
|
|
|
993
999
|
console.log(text);
|
|
994
1000
|
return { result, text };
|
|
995
1001
|
}
|
|
1002
|
+
async function planCliInput(args) {
|
|
1003
|
+
const subArgs = args.slice(1);
|
|
1004
|
+
const requestFromFlag = flagValue(args, "request");
|
|
1005
|
+
if (requestFromFlag) {
|
|
1006
|
+
return {
|
|
1007
|
+
repoPath: positionalRepoPath(subArgs),
|
|
1008
|
+
request: requestFromFlag,
|
|
1009
|
+
surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
|
|
1010
|
+
answers: keyValueFlag(args, "answer"),
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const positional = positionalValues(subArgs);
|
|
1014
|
+
const [first, ...rest] = positional;
|
|
1015
|
+
if (!first) {
|
|
1016
|
+
throw new Error("plan requires --request or a quoted request argument.");
|
|
1017
|
+
}
|
|
1018
|
+
const firstLooksLikePath = await pathExists(first);
|
|
1019
|
+
if (firstLooksLikePath && rest.length === 0) {
|
|
1020
|
+
throw new Error("plan requires --request when the only positional argument is a repository path.");
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
repoPath: firstLooksLikePath ? first : ".",
|
|
1024
|
+
request: firstLooksLikePath ? rest.join(" ").trim() : positional.join(" ").trim(),
|
|
1025
|
+
surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
|
|
1026
|
+
answers: keyValueFlag(args, "answer"),
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
996
1029
|
function toolResultStatus(printed) {
|
|
997
1030
|
try {
|
|
998
1031
|
const payload = JSON.parse(printed.text);
|
|
@@ -1002,6 +1035,125 @@ function toolResultStatus(printed) {
|
|
|
1002
1035
|
return undefined;
|
|
1003
1036
|
}
|
|
1004
1037
|
}
|
|
1038
|
+
function planSummary(payload) {
|
|
1039
|
+
const solutionPath = objectProp(payload, "solutionPath");
|
|
1040
|
+
const uikitCustomization = objectProp(payload, "uikitCustomization");
|
|
1041
|
+
const intake = objectProp(payload, "intake");
|
|
1042
|
+
const answers = objectProp(intake, "answers") ?? {};
|
|
1043
|
+
const workplan = objectProp(payload, "socialWorkplan");
|
|
1044
|
+
const questions = arrayProp(intake, "questions").map(questionSummary);
|
|
1045
|
+
const decisionsRequired = stringArrayProp(payload, "decisionsRequired");
|
|
1046
|
+
const solutionPathAnswerId = stringProp(solutionPath, "answerId");
|
|
1047
|
+
const uikitCustomizationAnswerId = stringProp(uikitCustomization, "answerId");
|
|
1048
|
+
return stripUndefined({
|
|
1049
|
+
kind: "plan-summary",
|
|
1050
|
+
outcome: stringProp(payload, "outcome"),
|
|
1051
|
+
platform: stringProp(payload, "platform"),
|
|
1052
|
+
supportLevel: stringProp(payload, "supportLevel"),
|
|
1053
|
+
solutionPath: solutionPath
|
|
1054
|
+
? {
|
|
1055
|
+
recommendation: stringProp(solutionPath, "recommendation"),
|
|
1056
|
+
confidence: stringProp(solutionPath, "confidence"),
|
|
1057
|
+
summary: stringProp(solutionPath, "summary"),
|
|
1058
|
+
answerId: solutionPathAnswerId,
|
|
1059
|
+
decisionRequired: booleanProp(objectProp(solutionPath, "decision"), "requiredBeforeHandRolledUi") === true
|
|
1060
|
+
&& !hasAnswerValue(answers, solutionPathAnswerId),
|
|
1061
|
+
}
|
|
1062
|
+
: undefined,
|
|
1063
|
+
uikitCustomization: uikitCustomization
|
|
1064
|
+
? {
|
|
1065
|
+
status: stringProp(uikitCustomization, "status"),
|
|
1066
|
+
recommendedLevel: stringProp(uikitCustomization, "recommendedLevel"),
|
|
1067
|
+
confidence: stringProp(uikitCustomization, "confidence"),
|
|
1068
|
+
summary: stringProp(uikitCustomization, "summary"),
|
|
1069
|
+
answerId: uikitCustomizationAnswerId,
|
|
1070
|
+
decisionRequired: booleanProp(objectProp(uikitCustomization, "decision"), "requiredBeforeCustomization") === true
|
|
1071
|
+
&& !hasAnswerValue(answers, uikitCustomizationAnswerId),
|
|
1072
|
+
}
|
|
1073
|
+
: undefined,
|
|
1074
|
+
intake: intake
|
|
1075
|
+
? {
|
|
1076
|
+
status: stringProp(intake, "status"),
|
|
1077
|
+
remainingBlocking: numberProp(intake, "remainingBlocking"),
|
|
1078
|
+
blockingQuestions: questions.filter((question) => question.blocksImplementationWhenMissing),
|
|
1079
|
+
openQuestions: questions,
|
|
1080
|
+
answers,
|
|
1081
|
+
}
|
|
1082
|
+
: undefined,
|
|
1083
|
+
decisionsRequired,
|
|
1084
|
+
workplan: workplan ? workplanSummary(workplan) : undefined,
|
|
1085
|
+
nextStep: stringProp(payload, "nextStep"),
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
function hasAnswerValue(answers, answerId) {
|
|
1089
|
+
if (!answerId) {
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
const answer = answers[answerId];
|
|
1093
|
+
return typeof answer === "string" && answer.trim() !== "";
|
|
1094
|
+
}
|
|
1095
|
+
function workplanSummary(workplan) {
|
|
1096
|
+
return stripUndefined({
|
|
1097
|
+
kind: stringProp(workplan, "kind"),
|
|
1098
|
+
status: stringProp(workplan, "status"),
|
|
1099
|
+
nextStep: stringProp(workplan, "nextStep"),
|
|
1100
|
+
surfaces: arrayProp(workplan, "sequence").map((surface) => {
|
|
1101
|
+
const surfaceObject = asObject(surface);
|
|
1102
|
+
const intake = objectProp(surfaceObject, "intake");
|
|
1103
|
+
return stripUndefined({
|
|
1104
|
+
id: stringProp(surfaceObject, "id"),
|
|
1105
|
+
order: numberProp(surfaceObject, "order"),
|
|
1106
|
+
outcome: stringProp(surfaceObject, "outcome"),
|
|
1107
|
+
label: stringProp(surfaceObject, "label"),
|
|
1108
|
+
remainingBlocking: numberProp(intake, "remainingBlocking"),
|
|
1109
|
+
intakeStatus: stringProp(intake, "status"),
|
|
1110
|
+
});
|
|
1111
|
+
}),
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
function questionSummary(value) {
|
|
1115
|
+
const question = asObject(value);
|
|
1116
|
+
return {
|
|
1117
|
+
id: stringProp(question, "id"),
|
|
1118
|
+
question: stringProp(question, "question"),
|
|
1119
|
+
required: booleanProp(question, "required"),
|
|
1120
|
+
blocksImplementationWhenMissing: booleanProp(question, "blocksImplementationWhenMissing"),
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
function objectProp(value, key) {
|
|
1124
|
+
if (!value) {
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
return asObject(value[key]);
|
|
1128
|
+
}
|
|
1129
|
+
function asObject(value) {
|
|
1130
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
1131
|
+
}
|
|
1132
|
+
function arrayProp(value, key) {
|
|
1133
|
+
if (!value) {
|
|
1134
|
+
return [];
|
|
1135
|
+
}
|
|
1136
|
+
const item = value[key];
|
|
1137
|
+
return Array.isArray(item) ? item : [];
|
|
1138
|
+
}
|
|
1139
|
+
function stringArrayProp(value, key) {
|
|
1140
|
+
return arrayProp(value, key).filter((item) => typeof item === "string");
|
|
1141
|
+
}
|
|
1142
|
+
function stringProp(value, key) {
|
|
1143
|
+
const item = value?.[key];
|
|
1144
|
+
return typeof item === "string" ? item : undefined;
|
|
1145
|
+
}
|
|
1146
|
+
function numberProp(value, key) {
|
|
1147
|
+
const item = value?.[key];
|
|
1148
|
+
return typeof item === "number" ? item : undefined;
|
|
1149
|
+
}
|
|
1150
|
+
function booleanProp(value, key) {
|
|
1151
|
+
const item = value?.[key];
|
|
1152
|
+
return typeof item === "boolean" ? item : undefined;
|
|
1153
|
+
}
|
|
1154
|
+
function stripUndefined(value) {
|
|
1155
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
1156
|
+
}
|
|
1005
1157
|
async function installSkill(args) {
|
|
1006
1158
|
const source = skillSourceDir();
|
|
1007
1159
|
const instructionInstall = instructionInstallDestination(args);
|
|
@@ -1438,7 +1590,12 @@ function ciCheckResult(result) {
|
|
|
1438
1590
|
};
|
|
1439
1591
|
}
|
|
1440
1592
|
function positionalRepoPath(args) {
|
|
1593
|
+
const values = positionalValues(args);
|
|
1594
|
+
return values[0] ?? ".";
|
|
1595
|
+
}
|
|
1596
|
+
function positionalValues(args) {
|
|
1441
1597
|
const flagsWithValues = new Set(["request", "requirements", "prototype", "variant", "variant-id", "brief", "brief-path", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source", "note", "kind", "sentiment", "metric"]);
|
|
1598
|
+
const values = [];
|
|
1442
1599
|
for (let index = 0; index < args.length; index += 1) {
|
|
1443
1600
|
const arg = args[index];
|
|
1444
1601
|
if (!arg) {
|
|
@@ -1454,9 +1611,18 @@ function positionalRepoPath(args) {
|
|
|
1454
1611
|
if (arg.startsWith("-")) {
|
|
1455
1612
|
continue;
|
|
1456
1613
|
}
|
|
1457
|
-
|
|
1614
|
+
values.push(arg);
|
|
1615
|
+
}
|
|
1616
|
+
return values;
|
|
1617
|
+
}
|
|
1618
|
+
async function pathExists(input) {
|
|
1619
|
+
try {
|
|
1620
|
+
await stat(path.resolve(expandHome(input)));
|
|
1621
|
+
return true;
|
|
1622
|
+
}
|
|
1623
|
+
catch {
|
|
1624
|
+
return false;
|
|
1458
1625
|
}
|
|
1459
|
-
return ".";
|
|
1460
1626
|
}
|
|
1461
1627
|
function requiredPositionalText(args, message) {
|
|
1462
1628
|
const values = [];
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const UIKIT_SIGNALS = [
|
|
2
|
+
{
|
|
3
|
+
id: "explicit-uikit",
|
|
4
|
+
label: "Explicit social.plus UIKit / UI Kit mention",
|
|
5
|
+
strength: "strong",
|
|
6
|
+
pattern: /\b(?:social\.plus\s+)?(?:ui\s?kit|uikit)\b(?!\s+(?:experience|expertise|knowledge|background|familiarity))/i,
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: "prebuilt-components",
|
|
10
|
+
label: "Prebuilt or ready-made social UI components",
|
|
11
|
+
strength: "strong",
|
|
12
|
+
pattern: /\b(pre[-\s]?built|ready[-\s]?made)\b|\b(?:ready[-\s]?to[-\s]?use|out[-\s]?of[-\s]?the[-\s]?box|plug[-\s]?and[-\s]?play|drop[-\s]?in|off[-\s]?the[-\s]?shelf)\b(?:\s+(?:social\.plus|social|messaging|message|notifications?|community|communities|chat|feeds?|profiles?|stor(?:y|ies)|comments?|users?|groups?|channels?|posts?|prebuilt|standard|ui\s?kit|uikit))?\s+(?:ui|ui\s?kit|uikit|components?|widgets?|screens?|views?|feed|chat|profile|comments?|communit(?:y|ies)|stor(?:y|ies)|surfaces?)\b/i,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "speed-to-launch",
|
|
16
|
+
label: "Launch speed / MVP pressure",
|
|
17
|
+
strength: "medium",
|
|
18
|
+
pattern: /\b(quick launch|launch quickly|ship quickly|ship fast|mvp|build (?:a |an |the )?prototype|prototype (?:quickly|fast|first)|rapid prototyp\w*|weeks not months|days not months|save (?:development|dev|build|engineering) time|reduce development time)\b/i,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "standard-layout",
|
|
22
|
+
label: "Standard social app layout or white-label surface",
|
|
23
|
+
strength: "medium",
|
|
24
|
+
pattern: /\b(standard social|standard layout|standard feed|standard chat|standard profile|white[-\s]?label|basic styling|minimal customization|moderate customization|brand colors only)\b/i,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "uikit-customization-ladder",
|
|
28
|
+
label: "UIKit customization mode",
|
|
29
|
+
strength: "medium",
|
|
30
|
+
pattern: /\b(dynamic ui|component styling|fork and extend|forked ui\s?kit|fork the ui\s?kit|customi[sz]e the ui\s?kit)\b/i,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
const REJECT_NEGATOR = "do not|don't|dont|won't|wont|can't|cant|cannot|avoid|skip|no|without|not|never";
|
|
34
|
+
const REJECT_CONNECTOR = "to|use|uses|using|used|want|wants|wanting|adopt|adopts|adopting|leverage|leveraging|rely|relying|touch|build|building|go|going|be|been|with|on";
|
|
35
|
+
const REJECT_UIKIT_NOUN = "(?:the\\s+)?(?:social\\.plus\\s+)?(?:ui\\s?kit|uikit)\\b";
|
|
36
|
+
const REJECT_SKILL_LOOKAHEAD = "(?!\\s+(?:experience|expertise|knowledge|background|familiarity))";
|
|
37
|
+
const REJECT_UIKIT_SOURCE = `\\b(?:${REJECT_NEGATOR})(?:\\s+(?:${REJECT_CONNECTOR}))*\\s+${REJECT_UIKIT_NOUN}${REJECT_SKILL_LOOKAHEAD}` +
|
|
38
|
+
`|\\b(?:instead of|rather than)(?:\\s+(?:${REJECT_CONNECTOR}))*\\s+${REJECT_UIKIT_NOUN}` +
|
|
39
|
+
`|\\b(?:anything|everything)\\s+but\\s+${REJECT_UIKIT_NOUN}`;
|
|
40
|
+
const NEGATED_FROM_SCRATCH = /\b(?:not|never|avoid|don't|dont|do not|rather than|instead of)\b[\s\w,]{0,16}?\bfrom scratch\b/i;
|
|
41
|
+
const NEGATED_STANDARD = /\b(?:not|never|avoid|don't|dont|do not|rather than|instead of)\b[\s\w,]{0,12}?\b(?:standard social|standard layout|standard feed|standard chat|standard profile|white[-\s]?label|basic styling|minimal customization|moderate customization)\b/i;
|
|
42
|
+
const SDK_SIGNALS = [
|
|
43
|
+
{
|
|
44
|
+
id: "direct-sdk",
|
|
45
|
+
label: "Direct SDK implementation",
|
|
46
|
+
strength: "strong",
|
|
47
|
+
pattern: /\b(?:directly|direct|using|use|with|via|embed|embedding|integrate|integrating)\s+(?:the\s+)?(?:social\.plus\s+)?SDK\b|\b(?:the\s+)?(?:social\.plus\s+)?SDK\s+for\b|\bSDK[-\s]?owned\b|\bSDK[-\s]?custom\b/i,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "reject-uikit",
|
|
51
|
+
label: "Explicitly rejects UIKit or chooses SDK instead of UIKit",
|
|
52
|
+
strength: "strong",
|
|
53
|
+
pattern: new RegExp(REJECT_UIKIT_SOURCE, "i"),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "custom-ui",
|
|
57
|
+
label: "Custom UI or non-standard experience",
|
|
58
|
+
strength: "strong",
|
|
59
|
+
pattern: /\b(?:custom|bespoke|unique|fully[-\s]?custom|completely[-\s]?custom|totally[-\s]?(?:different|custom)|brand[-\s]?new|differentiated)\s+(?:(?:social|messaging|notification|community|chat|feed|profile|story|stories|comment|comments|user|group|channel|post|navigation|onboarding|discovery)\s+)?(?:ui|interface|experience)\b|\bnon[-\s]?standard (?:ui|layout|flow|experience)\b|\bpixel[-\s]?perfect\b/i,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "custom-flows",
|
|
63
|
+
label: "Custom user flows or interactions",
|
|
64
|
+
strength: "strong",
|
|
65
|
+
pattern: /\b(custom flows?|custom user flows?|custom interactions?|custom workflows?|unique flows?|bespoke flows?|special interaction)\b/i,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "full-control",
|
|
69
|
+
label: "Full control / design freedom",
|
|
70
|
+
strength: "strong",
|
|
71
|
+
pattern: /\b(full control|complete control|maximum flexibility|complete design freedom|full design freedom|build from scratch|hand[-\s]?rolled|from scratch)\b/i,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "deep-app-integration",
|
|
75
|
+
label: "Deep existing-app integration",
|
|
76
|
+
strength: "medium",
|
|
77
|
+
pattern: /\b(deeply integrate|existing app flow|existing navigation|custom backend|server[-\s]?to[-\s]?server|non[-\s]?standard platform|advanced technical requirements)\b/i,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const SOURCE_EVIDENCE = [
|
|
81
|
+
"social.plus UIKit docs: prebuilt UI components, quick launch, and minimal/moderate customization paths.",
|
|
82
|
+
"social.plus SDK docs: custom experiences, unique user flows, complete design freedom, and existing-app integration.",
|
|
83
|
+
];
|
|
84
|
+
export function recommendSolutionPath(request, answers = {}) {
|
|
85
|
+
const text = typeof request === "string" ? request : "";
|
|
86
|
+
const explicitAnswer = normalizeSolutionPathAnswer(answers?.solution_path);
|
|
87
|
+
const sdkSignals = collectSignals(text, SDK_SIGNALS).filter((signal) => {
|
|
88
|
+
if (signal.id !== "full-control" || !/scratch/i.test(signal.matched)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return !NEGATED_FROM_SCRATCH.test(text);
|
|
92
|
+
});
|
|
93
|
+
const negativeUIKitSignals = sdkSignals.filter((signal) => signal.id === "reject-uikit");
|
|
94
|
+
let uikitScanText = text;
|
|
95
|
+
if (negativeUIKitSignals.length > 0) {
|
|
96
|
+
uikitScanText = text.replace(new RegExp(REJECT_UIKIT_SOURCE, "gi"), " ");
|
|
97
|
+
}
|
|
98
|
+
let uikitSignals = collectSignals(uikitScanText, UIKIT_SIGNALS).filter((signal) => {
|
|
99
|
+
if (signal.id !== "standard-layout") {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return !NEGATED_STANDARD.test(text);
|
|
103
|
+
});
|
|
104
|
+
if (negativeUIKitSignals.length > 0 && !uikitSignals.some((signal) => signal.strength === "strong")) {
|
|
105
|
+
uikitSignals = [];
|
|
106
|
+
}
|
|
107
|
+
if (explicitAnswer) {
|
|
108
|
+
return guidanceFor({
|
|
109
|
+
recommendation: explicitAnswer,
|
|
110
|
+
confidence: "high",
|
|
111
|
+
uikitSignals,
|
|
112
|
+
sdkSignals,
|
|
113
|
+
answered: explicitAnswer,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const hasStrongUIKit = uikitSignals.some((signal) => signal.strength === "strong");
|
|
117
|
+
const hasStrongSdk = sdkSignals.some((signal) => signal.strength === "strong");
|
|
118
|
+
if (uikitSignals.length > 0 && sdkSignals.length > 0) {
|
|
119
|
+
return guidanceFor({
|
|
120
|
+
recommendation: "needs-decision",
|
|
121
|
+
confidence: hasStrongUIKit || hasStrongSdk ? "medium" : "low",
|
|
122
|
+
uikitSignals,
|
|
123
|
+
sdkSignals,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (uikitSignals.length > 0) {
|
|
127
|
+
return guidanceFor({
|
|
128
|
+
recommendation: "uikit",
|
|
129
|
+
confidence: hasStrongUIKit ? "high" : "medium",
|
|
130
|
+
uikitSignals,
|
|
131
|
+
sdkSignals,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (sdkSignals.length > 0) {
|
|
135
|
+
return guidanceFor({
|
|
136
|
+
recommendation: "sdk",
|
|
137
|
+
confidence: hasStrongSdk ? "high" : "medium",
|
|
138
|
+
uikitSignals,
|
|
139
|
+
sdkSignals,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return guidanceFor({
|
|
143
|
+
recommendation: "sdk",
|
|
144
|
+
confidence: "medium",
|
|
145
|
+
uikitSignals,
|
|
146
|
+
sdkSignals,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function guidanceFor(args) {
|
|
150
|
+
const decision = {
|
|
151
|
+
requiredBeforeHandRolledUi: !args.answered && (args.recommendation === "uikit" || args.recommendation === "hybrid" || args.recommendation === "needs-decision"),
|
|
152
|
+
question: "Should this build use social.plus UIKit components, a direct SDK implementation, or a hybrid path?",
|
|
153
|
+
options: [
|
|
154
|
+
"uikit: use social.plus UIKit for standard social surfaces and theme/customize it",
|
|
155
|
+
"sdk: build the experience directly with the social.plus SDK",
|
|
156
|
+
"hybrid: use UIKit for standard surfaces and SDK/custom code for differentiated app-layer behavior",
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
if (args.recommendation === "uikit") {
|
|
160
|
+
return {
|
|
161
|
+
recommendation: "uikit",
|
|
162
|
+
confidence: args.confidence,
|
|
163
|
+
summary: args.answered === "uikit"
|
|
164
|
+
? "The customer explicitly selected the UIKit path. Confirm scope, then use social.plus UIKit as the primary implementation artifact instead of hand-rolling standard social UI."
|
|
165
|
+
: "The request sounds UIKit-shaped: favor social.plus UIKit for the standard social surface, then customize within the UIKit theming/component model before considering direct SDK UI.",
|
|
166
|
+
answerId: "solution_path",
|
|
167
|
+
signals: { uikit: args.uikitSignals, sdk: args.sdkSignals },
|
|
168
|
+
decision,
|
|
169
|
+
implementationGuidance: [
|
|
170
|
+
"Confirm `solution_path=uikit` before the host agent hand-rolls standard feed, chat, profile, comments, community, or story UI.",
|
|
171
|
+
"Use UIKit installation, authentication, customization, and platform build guidance as the implementation source of truth.",
|
|
172
|
+
"Keep Vise's existing SDK setup, secrets, region, auth, build, and project-sensor guidance around the UIKit integration.",
|
|
173
|
+
"Do not claim deterministic compliance for UIKit internals hidden outside the customer repo; validate customer wiring, configuration, custom code, and local build evidence.",
|
|
174
|
+
"Keep design conformance advisory: use Dynamic UI, component styling, or fork-and-extend based on the customer's customization need.",
|
|
175
|
+
],
|
|
176
|
+
evidence: SOURCE_EVIDENCE,
|
|
177
|
+
advisoryOnly: "This is an advisory solution-path recommendation. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (args.recommendation === "needs-decision") {
|
|
181
|
+
return {
|
|
182
|
+
recommendation: "needs-decision",
|
|
183
|
+
confidence: args.confidence,
|
|
184
|
+
summary: args.answered === "hybrid"
|
|
185
|
+
? "The customer selected a hybrid path. Treat UIKit as the default for standard surfaces and use direct SDK/custom code only for the differentiated behavior."
|
|
186
|
+
: "The request contains both UIKit-shaped speed/prebuilt signals and SDK-shaped custom-control signals. Ask for the solution path before the agent starts UI implementation.",
|
|
187
|
+
answerId: "solution_path",
|
|
188
|
+
signals: { uikit: args.uikitSignals, sdk: args.sdkSignals },
|
|
189
|
+
decision,
|
|
190
|
+
implementationGuidance: [
|
|
191
|
+
"Resolve `solution_path` before hand-rolling standard social UI.",
|
|
192
|
+
"Choose UIKit when the standard social surface can be themed or lightly customized.",
|
|
193
|
+
"Choose SDK when the requested workflow, layout, or interaction model is materially different from UIKit's component model.",
|
|
194
|
+
"Choose hybrid when UIKit can cover baseline social surfaces and SDK/custom code is needed for app-specific extensions.",
|
|
195
|
+
"Do not claim deterministic compliance for UIKit internals hidden outside the customer repo; validate customer wiring, configuration, custom code, and local build evidence.",
|
|
196
|
+
],
|
|
197
|
+
evidence: SOURCE_EVIDENCE,
|
|
198
|
+
advisoryOnly: "This is an advisory solution-path recommendation. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (args.recommendation === "hybrid") {
|
|
202
|
+
return {
|
|
203
|
+
recommendation: "hybrid",
|
|
204
|
+
confidence: args.confidence,
|
|
205
|
+
summary: "The customer selected a hybrid path. Treat UIKit as the default for standard surfaces and use direct SDK/custom code only for the differentiated behavior.",
|
|
206
|
+
answerId: "solution_path",
|
|
207
|
+
signals: { uikit: args.uikitSignals, sdk: args.sdkSignals },
|
|
208
|
+
decision,
|
|
209
|
+
implementationGuidance: [
|
|
210
|
+
"Use UIKit for standard social surfaces that fit its component and customization model.",
|
|
211
|
+
"Use direct SDK/custom code only for differentiated app-layer behavior, custom profile/discovery surfaces, or non-standard flows.",
|
|
212
|
+
"Keep the handoff explicit in the implementation summary: name which surfaces are UIKit-owned and which are SDK/customer-owned.",
|
|
213
|
+
"Do not claim deterministic compliance for UIKit internals hidden outside the customer repo; validate customer wiring, configuration, custom code, and local build evidence.",
|
|
214
|
+
],
|
|
215
|
+
evidence: SOURCE_EVIDENCE,
|
|
216
|
+
advisoryOnly: "This is an advisory solution-path recommendation. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
recommendation: "sdk",
|
|
221
|
+
confidence: args.confidence,
|
|
222
|
+
summary: args.answered === "sdk"
|
|
223
|
+
? "The customer explicitly selected the direct SDK path. Continue with the normal Vise SDK integration harness."
|
|
224
|
+
: args.sdkSignals.some((signal) => signal.id === "reject-uikit")
|
|
225
|
+
? "The request explicitly rejects UIKit or chooses the SDK instead of UIKit, so the normal SDK-first Vise harness is the safer fit."
|
|
226
|
+
: args.sdkSignals.length > 0
|
|
227
|
+
? "The request asks for custom control, unique flows, or deep app integration, so the normal SDK-first Vise harness is the safer fit."
|
|
228
|
+
: "No strong UIKit fast-launch/prebuilt-surface signal was detected, so Vise keeps the existing SDK-first integration flow.",
|
|
229
|
+
answerId: "solution_path",
|
|
230
|
+
signals: { uikit: args.uikitSignals, sdk: args.sdkSignals },
|
|
231
|
+
decision,
|
|
232
|
+
implementationGuidance: [
|
|
233
|
+
"Use the normal Vise SDK integration flow: plan, answer intake, initialize the compliance contract, implement, check, and run sensors.",
|
|
234
|
+
"If the customer later asks for a standard prebuilt social surface or quick-launch MVP, re-plan with `--answer solution_path=uikit` or a UIKit-specific request.",
|
|
235
|
+
],
|
|
236
|
+
evidence: SOURCE_EVIDENCE,
|
|
237
|
+
advisoryOnly: "This is an advisory solution-path recommendation. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function collectSignals(request, signals) {
|
|
241
|
+
return signals.flatMap((signal) => {
|
|
242
|
+
const match = request.match(signal.pattern);
|
|
243
|
+
if (!match?.[0]) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
return [{
|
|
247
|
+
id: signal.id,
|
|
248
|
+
label: signal.label,
|
|
249
|
+
strength: signal.strength,
|
|
250
|
+
matched: match[0],
|
|
251
|
+
}];
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const REJECT_UIKIT_IN_ANSWER = new RegExp(REJECT_UIKIT_SOURCE, "i");
|
|
255
|
+
const REJECT_HYBRID_IN_ANSWER = /\b(?:not|no|without|avoid|don't|dont|do not|never|rather than|instead of)\b[\s\w,]{0,12}?\bhybrid\b/;
|
|
256
|
+
function normalizeSolutionPathAnswer(answer) {
|
|
257
|
+
const normalized = (typeof answer === "string" ? answer : "").trim().toLowerCase();
|
|
258
|
+
if (!normalized) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
if (/\bhybrid\b/.test(normalized) && !REJECT_HYBRID_IN_ANSWER.test(normalized)) {
|
|
262
|
+
return "hybrid";
|
|
263
|
+
}
|
|
264
|
+
if (REJECT_UIKIT_IN_ANSWER.test(normalized)) {
|
|
265
|
+
return "sdk";
|
|
266
|
+
}
|
|
267
|
+
if (/\b(ui\s?kit|uikit)\b/.test(normalized)) {
|
|
268
|
+
return "uikit";
|
|
269
|
+
}
|
|
270
|
+
if (/\bsdk\b/.test(normalized)) {
|
|
271
|
+
return "sdk";
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|