@amityco/social-plus-vise 0.12.5 → 0.14.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 +79 -0
- package/README.md +42 -32
- package/dist/server.js +49 -2
- package/dist/tools/ast.js +48 -8
- package/dist/tools/compliance.js +4 -1
- package/dist/tools/design.js +410 -0
- package/dist/tools/harness.js +2 -2
- package/dist/tools/integration.js +27 -5
- package/dist/tools/project.js +92 -8
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,85 @@ 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
|
+
## 0.14.0 — 2026-06-03
|
|
8
|
+
|
|
9
|
+
**Theme:** DesignBuildBrief — plan-time, grounded UI-building guidance for coding agents (advisory).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **DesignBuildBrief in `vise plan`** (`designContract.brief`): conservative semantic token roles inferred from token NAMES only (noun-first compounds — `--text-primary` binds `textPrimary`, never `primaryAction`; value-based inference is forbidden), token-derived component hints (card/button/input) with explicit absent variants, and grounded do/avoid lines — every line cites the declared tokens/roles it derives from, and an ungrounded line is structurally impossible. For `add-feed` / `add-chat` outcomes the brief carries a conditional **outcome recipe** whose items only reference roles that were actually inferred. Generated at plan time — never persisted to `sp-vise/`, never part of any digest.
|
|
13
|
+
- **Non-blocking `primary_action_token` intake question** when a design contract exists for a feed/chat outcome but no primary-action token was confidently identified — the agent asks instead of guessing.
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
- Advisory only; never gates `vise check`. **No design-conformance improvement is claimed for the brief yet** — a pre-registered ablation (`benchmarks/brief-ablation/PROTOCOL.md`: 0.13.0 vs 0.14.0, same contract, n=3, Cursor/Composer 2.5) measures its effect; documentation claims will follow the measurement.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 0.13.0 — 2026-06-03
|
|
21
|
+
|
|
22
|
+
**Theme:** Deterministic-gate soundness, CLI reliability, and post-rebrand coherence (driven by two repo reviews).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **Boolean flags now accept `--flag=true/false`.** `--ci=true`, `--dry-run=true`, `--force=true`, etc. were parsed as known flags but never detected, so `vise check --ci=true` silently ran in non-CI mode without erroring. Boolean flags now parse `=true/1/yes` → on and `=false/0/no` → off, and reject other values loudly.
|
|
26
|
+
- **`vise install-skill` supersedes pre-rebrand installs.** Installing removes a stale sibling `social-plus-foundry/` skill directory (and `social-plus-foundry.mdc` for `cursor-rules`) so hosts no longer serve old `spf`/foundry guidance alongside the current skill. Reported as `supersededLegacy` in the command output.
|
|
27
|
+
- **CLI no longer crashes on a missing tree-sitter native binding** — the parser loads lazily and degrades to regex-only instead of taking down every command (including doc lookup) at startup; affects platforms without prebuilt binaries (linux-arm64, Alpine/musl, win32-arm64).
|
|
28
|
+
- **Gating literal checks are comment-aware** — a commented-out or documented `channelId`/`apiKey` no longer trips a no-escape gate (a false positive that hard-failed CI with no attestation path). ts/tsx/kotlin use the tree-sitter stripper; Swift/Dart use a conservative string-aware scanner that only blanks comment spans.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **`vise_version`** in `sp-vise/compliance.json`, attestations, `engagement.json`, and the design contract — written alongside the retained `foundry_version` (backward-compatible alias; excluded from all digests, so existing contracts do not drift).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- **Docs/copy aligned to the `Vise` name:** README, skill, and the generated sidecar no longer reference a non-existent `sp-check` binary (use `vise check`); `run-sensors` safety wording corrected ("runs detected project scripts/wrappers; inspect with `--dry-run` before running in an untrusted project"); `RULES.md` gating semantics corrected — the exit code is driven by `advisory`/`attestation.allowed`, not `severity`.
|
|
35
|
+
- **README benchmark section recalibrated** to match the Commune paper: N=1 caveat moved up front, the deterministic-grader/circularity disclosed, the speculative "rework sessions" table removed, and the null bug-fix-benchmark result noted.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 0.12.5 — 2026-06-02
|
|
40
|
+
|
|
41
|
+
**Theme:** Design token lifecycle — customers can maintain a dedicated social.plus token file independently from their main app, with no AI agent required for updates.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- **`vise design init-tokens [path] [--force]`** — scaffolds `src/styles/social-plus-tokens.css` in the customer's project, the single editable source for social.plus feature styling. **Greenfield:** full `--sp-*` neutral defaults (color, typography, spacing, radius, shadow, motion, sizing, z-index, breakpoints). **Brownfield:** seeded from the project's existing concrete token values, namespaced as `--sp-*` with origin comments. Idempotent — never clobbers an existing file; `--force` to override. The token file lives in `src/` (ships with the app); `sp-vise/` holds only the contract (never ships).
|
|
45
|
+
- **Freshness check in `vise design check`** — hashes `source.inputs` files at extract time, stores as `source.input_digests` in the contract. On `design check`, current file hashes are compared; if any changed, an advisory `staleContract` field surfaces a nudge: *"Run `vise design extract --from-project` to refresh."* Never blocks; purely informational. Works identically across all platforms.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 0.12.4 — 2026-06-02
|
|
50
|
+
|
|
51
|
+
**Theme:** Advisory rule model + honest benchmark methodology.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- **Advisory rule flag** (`advisory: true` in rule YAML) — rules marked advisory surface in `vise check` output with `status: "advisory"` but never contribute to `exitCode` or `needs-attestation`. Use for checks where the right answer depends on tenant configuration Vise cannot observe.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
- **`reactions.configured-name-used`** (all 5 platforms) downgraded to advisory. Rule fires on ~100% of correct apps (every tenant defaults to `"like"`) and was only clearable by a ritual comment. Version bumped to 2; existing compliance.json files will show contract-drift — run `vise sync` to update.
|
|
58
|
+
|
|
59
|
+
### Benchmark
|
|
60
|
+
- Brand benchmark (Spotify Encore × social.plus community feed, Sonnet n=3): pure-mcp 0/3 behavioral compliance (avg 2.3 behavioral findings), vise-design 3/3 (0). Ban-state is the standout discriminator — missed by all pure-mcp agents, fixed by all vise-design agents through the iteration loop.
|
|
61
|
+
- Ambiguous-brief design test: vise-design 0 hex literals (all 3 seeds), pure-mcp 0/2/15 (high variance). Design loop is a variance-reduction tool.
|
|
62
|
+
- Grader now partitions findings into behavioral / file-presence / attestation-dialect; headline score is behavioral-only.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 0.12.3 — 2026-06-02
|
|
67
|
+
|
|
68
|
+
**Theme:** Design harness graduation — complete token extraction, Circles-inspired visual reference.
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
- **`vise design reference [path] [--title <name>]`** — generates a self-contained `sp-vise/design-reference.html` design-system spec: fixed sidebar with grouped nav (COLOR / TYPOGRAPHY / LAYOUT / SURFACE / EFFECTS), sticky topbar, section headers with display-font titles, monospace token-row lists for non-visual groups (motion, breakpoints, z-index), and component samples. Reads source CSS for live `var()` resolution; falls back to contract tokens for native projects (Android/Flutter/iOS correctly grouped by category with concrete values).
|
|
72
|
+
- **6 new token categories** — `fontWeight`, `lineHeight`, `letterSpacing`, `borderWidth`, `breakpoint`, `zIndex`. All name-gated to prevent false positives (bare integers, unitless decimals are ambiguous without the name signal).
|
|
73
|
+
|
|
74
|
+
### Fixed (silent miscategorizations)
|
|
75
|
+
- `--fs-*` (e.g. `--fs-sm: 14px`) was categorized as "space" — now correctly "fontSize".
|
|
76
|
+
- `--border-width-*` (e.g. `--border-width-thin: 1px`) was categorized as "color" (the color-name regex matched `/border/`) — now correctly "borderWidth". Fix requires the borderWidth check to precede the color branch.
|
|
77
|
+
- `--bp-*` and `--ls-*` fell through to the LENGTH→space fallback — now correctly "breakpoint" and "letterSpacing".
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
- `renderDesignPreview` now includes sections for all 6 new categories so they aren't silently omitted from `vise design preview` output.
|
|
81
|
+
- `categorizeTokenModuleValue` explicitly excludes the new categories (CSS-only extraction — consistent with the existing opacity exclusion).
|
|
82
|
+
- Streamly seed-ui reference: **69 → 80 tokens**, zero "not extracted" tags in the generated HTML.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
7
86
|
## 0.12.2 — 2026-06-02
|
|
8
87
|
|
|
9
88
|
**Maintenance / hygiene release.** No functional change from `0.12.1` — identical rules, validators, and CLI. This release exists to scrub an anonymized customer name from the bundled `CHANGELOG`; `0.12.0` and `0.12.1` (which contained it) were unpublished from npm. Use `0.12.2`.
|
package/README.md
CHANGED
|
@@ -47,6 +47,22 @@ Instead of just providing a CLI or AI skills, Vise implements a technique called
|
|
|
47
47
|
|
|
48
48
|
Vise acts as the foreman of this factory, wrapping your local coding agents in compliance guardrails when they integrate social.plus SDKs. It inspects your project, grounds the agent in hosted docs, enforces 300 platform-specific compliance rules, checks the generated UI against the customer's design system, surfaces the full SDK feature surface so nothing is silently dropped, and runs your project's own build/lint/typecheck sensors. **Your source code never leaves your machine.**
|
|
49
49
|
|
|
50
|
+
At a glance, Vise sits between the user's prompt and the agent's code changes. The agent still edits the app; Vise turns the request into a grounded plan, records the local contract, and keeps checking until the integration is ready to ship.
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
flowchart LR
|
|
54
|
+
Prompt["User prompt<br/>Add a social.plus feature"] --> Skill["AI skill<br/>drives the loop"]
|
|
55
|
+
Skill --> Inspect["Inspect project<br/>platform, app surface,<br/>design signals"]
|
|
56
|
+
Inspect --> Plan["Plan<br/>outcome, docs,<br/>intake questions"]
|
|
57
|
+
Plan --> Design["Design + completeness<br/>tokens, feature checklist,<br/>explicit opt-outs"]
|
|
58
|
+
Design --> Build["Agent builds<br/>edits customer code locally"]
|
|
59
|
+
Build --> Check["Vise check<br/>SDK compliance gate"]
|
|
60
|
+
Check -->|findings| Build
|
|
61
|
+
Check --> Sensors["Sensors<br/>typecheck, build,<br/>lint, SDK smoke"]
|
|
62
|
+
Sensors -->|failures| Build
|
|
63
|
+
Sensors --> Done["Done<br/>sp-vise contract<br/>and evidence"]
|
|
64
|
+
```
|
|
65
|
+
|
|
50
66
|
| Layer | Purpose |
|
|
51
67
|
|---|---|
|
|
52
68
|
| **Skill** (`SKILL.md`) | Tells your AI agent when to inspect, plan, fetch docs, edit, validate, and attest |
|
|
@@ -59,7 +75,7 @@ Vise validates on three layers, and the layer is set by the *kind of claim* —
|
|
|
59
75
|
|
|
60
76
|
| Layer | Claim | How | Enforcement |
|
|
61
77
|
|---|---|---|---|
|
|
62
|
-
| **SDK compliance** | "this is **wrong**" | 300 deterministic rules (session renewal, live-collection vs one-shot, no secret in logs, parent-child rendering, ban-state gating…) | **Hard gate** — `vise check` blocks until green or attested |
|
|
78
|
+
| **SDK compliance** | "this is **wrong**" | 300 deterministic rules (session renewal, live-collection vs one-shot, no secret in logs, parent-child rendering, ban-state gating…) | **Hard gate** — `vise check` blocks until green or attested. A small advisory subset surfaces as informational only and never blocks. |
|
|
63
79
|
| **Design conformance** | "this **looks off**" | extract the customer's design system into a contract, then check token usage | **Advisory** — `vise design check`/`preview`; never fails a build |
|
|
64
80
|
| **Feature completeness** | "this is **missing**" | Vise proposes the full SDK feature surface per outcome; the agent opts out of anything out of scope with a recorded reason | **Advisory** — surfaced in `vise plan`/`check`; never fails a build |
|
|
65
81
|
|
|
@@ -67,7 +83,9 @@ Only correctness is gated (it can be made FP-free); conformance and completeness
|
|
|
67
83
|
|
|
68
84
|
### Design-conformant UI
|
|
69
85
|
|
|
70
|
-
Vise can ingest the customer's aesthetic into a **design contract** and guide generation to match it — from an HTML/CSS prototype (`vise design extract`) or from the host app's own design system across web + Android + Flutter + iOS (`vise design extract --from-project`: CSS vars/Tailwind/token modules, `colors.xml`, Flutter `Color(0x…)`, iOS `.colorset`/Swift). `vise design check` reports token conformance; `vise design preview` writes a visual review. All advisory.
|
|
86
|
+
Vise can ingest the customer's aesthetic into a **design contract** and guide generation to match it — from an HTML/CSS prototype (`vise design extract`) or from the host app's own design system across web + Android + Flutter + iOS (`vise design extract --from-project`: CSS vars/Tailwind/token modules, `colors.xml`, Flutter `Color(0x…)`, iOS `.colorset`/Swift). `vise design check` reports token conformance; `vise design preview` writes a visual review; `vise design reference` generates a full visual design-system spec (swatches, type samples, component demos). All advisory.
|
|
87
|
+
|
|
88
|
+
**For social.plus-specific styling:** `vise design init-tokens` scaffolds `src/styles/social-plus-tokens.css` in your project — a dedicated token file for social.plus features that you can edit independently from your main app's design system. Greenfield projects get sensible `--sp-*` defaults; brownfield projects get their existing token values seeded in. Edit the file, run `vise design extract --from-project` to refresh the contract, and future agent builds inherit the updated palette — no AI agent needed in the update loop.
|
|
71
89
|
|
|
72
90
|
### Supported integrations (outcomes)
|
|
73
91
|
|
|
@@ -81,8 +99,8 @@ A bench vise holds the workpiece steady so the craftsman's hands are free to sha
|
|
|
81
99
|
|
|
82
100
|
## Benchmark: Phase 1 Results
|
|
83
101
|
|
|
84
|
-
> **
|
|
85
|
-
>
|
|
102
|
+
> **The compliance gaps agents ship on their own, they close under Vise's check loop.**
|
|
103
|
+
> Across two capable coding agents (Cursor / Composer 2.5 and Claude Sonnet 4.6), the features with *secondary* compliance requirements — Chat, Moderation, Push — failed without Vise and passed with it; both agents reached 9/9 with Vise. This is a **strong directional signal at N=1 per cell, not a settled statistical finding.** The [Commune paper](docs/commune-paper-2026-05-30.md) is the full, honest version — methodology, per-cell results, threats to validity, and a complementary bug-fix benchmark where Vise showed *no* advantage.
|
|
86
104
|
|
|
87
105
|
### What "delivered correctly" means
|
|
88
106
|
|
|
@@ -93,7 +111,7 @@ A bench vise holds the workpiece steady so the craftsman's hands are free to sha
|
|
|
93
111
|
- **Moderation actions** (report, flag, block) are surfaced in the UI so users can act on them, not buried in a hook
|
|
94
112
|
- **Chat and feed queries** use live, reactive subscriptions — not one-time fetches that go stale
|
|
95
113
|
|
|
96
|
-
Without Vise, AI agents frequently implement the primary feature correctly but miss these secondary requirements. They know about them in the abstract — but when building a chat screen, "ban state" feels out of scope and gets skipped. `
|
|
114
|
+
Without Vise, AI agents frequently implement the primary feature correctly but miss these secondary requirements. They know about them in the abstract — but when building a chat screen, "ban state" feels out of scope and gets skipped. `vise check` turns that vague awareness into a specific, actionable finding.
|
|
97
115
|
|
|
98
116
|
### The experiment: three conditions, nine features
|
|
99
117
|
|
|
@@ -106,7 +124,7 @@ SDK setup · User presence · Social feed · Events · Chat & DMs · Push notifi
|
|
|
106
124
|
|---|---|---|
|
|
107
125
|
| **Pure MCP** | Access to social.plus docs only — no compliance guidance | Baseline: how well does the agent do on its own? |
|
|
108
126
|
| **Rules-as-Markdown** | The full 1,013-line compliance rulebook pasted directly into the prompt | Is the problem just that the agent doesn't know the rules? |
|
|
109
|
-
| **Vise + Skill** | Full Vise CLI — `
|
|
127
|
+
| **Vise + Skill** | Full Vise CLI — `vise check` runs automatically, agent reads specific findings, fixes them, repeats until green | Does an active feedback loop change the outcome? |
|
|
110
128
|
|
|
111
129
|
The Rules-as-Markdown condition is the key isolation: if the agent already knows all the rules, does giving it the spec document fix the problem? The answer turned out to be **no** — knowing the rules and being forced to act on specific findings are different things.
|
|
112
130
|
|
|
@@ -117,38 +135,28 @@ The Rules-as-Markdown condition is the key isolation: if the agent already knows
|
|
|
117
135
|
| **Cursor (Composer 2.5)** | 6 out of 9 ✗ | 5 out of 9 ✗ | **9 out of 9 ✅** |
|
|
118
136
|
| **Claude Code (Sonnet 4.6)** | 6 out of 9 ✗ | 7 out of 9 ✗ | **9 out of 9 ✅** |
|
|
119
137
|
|
|
120
|
-
The three features that consistently fail without Vise — **Chat**, **Moderation**, and **Push Notifications** — are exactly the ones with secondary compliance requirements (ban-state, report affordances, Amity preference API).
|
|
121
|
-
|
|
122
|
-
Both agents reached a perfect score with Vise. Neither could reach it with the compliance spec pasted into the prompt. All 9 passes were independently verified by code inspection — no scoring shortcuts.
|
|
138
|
+
The three features that consistently fail without Vise — **Chat**, **Moderation**, and **Push Notifications** — are exactly the ones with secondary compliance requirements (ban-state, report affordances, Amity preference API). `vise check` catches these with a specific finding; the rules doc does not.
|
|
123
139
|
|
|
124
|
-
|
|
140
|
+
Both agents reached 9/9 with Vise. The Rules-as-Markdown arm did **not** reliably beat the plain-docs control — 5/9 on Cursor (*below* control) and 7/9 on Sonnet — and at N=1 per cell neither gap is distinguishable from noise. The robust, reproducible signal is narrower and mechanistic: **Chat and Moderation never pass under either control arm, and always pass under Vise.** Passes were scored by a deterministic grader, not by hand — see [Reproducibility & honest caveats](#reproducibility--honest-caveats) for what that grader does and doesn't establish.
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
| Coding agent (model) | Condition | Features correct | Rework sessions needed |
|
|
129
|
-
|---|---|---|---|
|
|
130
|
-
| **Cursor (Composer 2.5)** | Pure MCP | 6 / 9 ✗ | +3 or more |
|
|
131
|
-
| **Cursor (Composer 2.5)** | Rules-as-Markdown | 5 / 9 ✗ | +4 or more |
|
|
132
|
-
| **Cursor (Composer 2.5)** | **Vise + Skill** | **9 / 9 ✅** | **0 ✅** |
|
|
133
|
-
| **Claude Code (Sonnet 4.6)** | Pure MCP | 6 / 9 ✗ | +3 or more |
|
|
134
|
-
| **Claude Code (Sonnet 4.6)** | Rules-as-Markdown | 7 / 9 ✗ | +2 or more |
|
|
135
|
-
| **Claude Code (Sonnet 4.6)** | **Vise + Skill** | **9 / 9 ✅** | **0 ✅** |
|
|
142
|
+
### Why it matters
|
|
136
143
|
|
|
137
|
-
|
|
144
|
+
A failing feature without Vise is *invisible* until a user hits it: the code compiles, the demo works, and the ban-state gap surfaces only when a banned user posts. Vise turns that latent gap into a specific finding the agent fixes before you ship. (We did not separately measure remediation effort, so this makes no rework-cost claim — only that the gaps are real and silent without a checker.)
|
|
138
145
|
|
|
139
|
-
### Reproducibility
|
|
146
|
+
### Reproducibility & honest caveats
|
|
140
147
|
|
|
141
|
-
- **
|
|
142
|
-
- **
|
|
143
|
-
- **Three arms
|
|
144
|
-
- **
|
|
145
|
-
-
|
|
148
|
+
- **Scoring is deterministic — and it overlaps with what Vise enforces.** Each cell is graded on four dimensions: `vise check --ci` (the same compliance ruleset), the project's own sensors (build / typecheck / lint), and hand-authored string-inclusion acceptance patterns. Because the metric overlaps Vise's own rules — and only the Vise arm iterates against that checker — read the headline as "Vise's checks pass," not as a fully independent oracle. The acceptance patterns are literal string matching (not AST), so they involve authoring judgment.
|
|
149
|
+
- **Vise-arm passes were deterministic-pass**, not attestation exceptions — agents fixed the code. (The grader applies a narrow, *symmetric* auto-attestation for absence / type-stub findings across **all** arms including the controls; it cannot satisfy the acceptance patterns, so it does not tilt the result toward Vise.)
|
|
150
|
+
- **Three arms, separate tooling.** The Rules-as-Markdown arm has no Vise checker available — it cannot run `vise check`.
|
|
151
|
+
- **Built from scratch** (greenfield seed), capable models with prior SDK familiarity. A complementary **bug-fix** benchmark showed **no Vise advantage** — the loop helps on greenfield integration, not local bug hunts.
|
|
152
|
+
- **N=1 per cell.** A strong directional signal (the Chat/Moderation/Push mechanism reproduces across both models), **not** a statistically settled finding; repeatability seeds are pending.
|
|
153
|
+
- **Full methodology, per-cell analysis, and threats to validity:** [the Commune paper](docs/commune-paper-2026-05-30.md). The [`benchmarks/FINDINGS.html`](benchmarks/FINDINGS.html) and [`benchmarks/RULES_AS_MARKDOWN.html`](benchmarks/RULES_AS_MARKDOWN.html) files are **summary report tables**, not raw transcripts or workspace diffs.
|
|
146
154
|
|
|
147
155
|
### Which mode should I use?
|
|
148
156
|
|
|
149
157
|
| If you… | Use | Why |
|
|
150
158
|
|---|---|---|
|
|
151
|
-
| Building new social features with an AI agent | **Vise CLI + Skill** | The
|
|
159
|
+
| Building new social features with an AI agent | **Vise CLI + Skill** | The mode that closed every secondary-compliance gap in our benchmark |
|
|
152
160
|
| Auditing existing social.plus code | `vise check --ci` | Grades any codebase against the full ruleset |
|
|
153
161
|
| Enforcing compliance in a CI pipeline | `vise check --ci` | Exits non-zero on failures; structured JSON output for logs |
|
|
154
162
|
|
|
@@ -162,7 +170,7 @@ Vise delivers all 9 features correctly in a single session. The other conditions
|
|
|
162
170
|
| **React Native** | ✅ Full | `tsc`, `npm lint`, SDK import smoke |
|
|
163
171
|
| **Flutter / Dart** | ✅ Full | `flutter analyze`, `flutter test` |
|
|
164
172
|
| **Android (Kotlin)** | ✅ Full | Gradle assemble, unit tests |
|
|
165
|
-
| **iOS (Swift)** | ✅ Full |
|
|
173
|
+
| **iOS (Swift)** | ✅ Full | Static rule checks fully operational. Build sensor not wired (`xcodebuild` environment requirements make it fragile) — `vise run-sensors` returns no-sensors for iOS; compliance rules run regardless. |
|
|
166
174
|
|
|
167
175
|
Each platform has 52–54 rules across 10 compliance domains (feed, comments, moderation, chat, secrets, session & auth, notifications, live objects, logging hygiene, design tokens).
|
|
168
176
|
|
|
@@ -251,7 +259,7 @@ The flow above is what the skill teaches your AI agent. You — the human — dr
|
|
|
251
259
|
| `vise design check [path]` | Advisory, **non-blocking** report on how closely the UI code matches the contract (token coverage + on/off-contract color literals). Never fails a build and is **not** a `vise check` gate |
|
|
252
260
|
| `vise design preview [path] [--reference <prototype>]` | Write a self-contained `sp-vise/design-preview.html`: the contract's tokens as visual swatches + the conformance report + the HTML reference embedded for side-by-side review. Vise renders the artifact; a human/VLM judges the visual match. Dependency-free — **not** an automated pixel diff |
|
|
253
261
|
| `vise design reference [path] [--title <name>]` | Write a self-contained `sp-vise/design-reference.html`: human/VLM-readable design-system spec — token swatches, type samples, component demos, and a growth-layer summary. Pairs with `design-contract.json` (machine-readable). Use `--title` to name the design system (e.g. `--title Streamly`). Advisory — **not** an enforcement gate |
|
|
254
|
-
| `vise design init-tokens [path] [--force]` | Scaffold `src/styles/social-plus-tokens.css` — the dedicated, customer-editable token file for social.plus features. **Greenfield:** neutral defaults (full `--sp-*` token set). **Brownfield:** seeded from your existing concrete tokens. Idempotent — never overwrites an existing file (use `--force` to override). After editing, run `vise design extract --from-project` to refresh the contract
|
|
262
|
+
| `vise design init-tokens [path] [--force]` | Scaffold `src/styles/social-plus-tokens.css` — the dedicated, customer-editable token file for social.plus features. **Greenfield:** neutral defaults (full `--sp-*` token set). **Brownfield:** seeded from your existing concrete tokens. Idempotent — never overwrites an existing file (use `--force` to override). After editing, run `vise design extract --from-project` to refresh the contract |
|
|
255
263
|
|
|
256
264
|
The extracted contract is **advisory input for generation**, not an enforcement gate: a token-poor prototype yields a weaker — never wrong — contract, and absence of a prototype simply means no contract (the existing `*.design.reuse-detected-tokens` rules still cover reuse of a host project's own design system).
|
|
257
265
|
|
|
@@ -279,7 +287,7 @@ The extracted contract is **advisory input for generation**, not an enforcement
|
|
|
279
287
|
|
|
280
288
|
| Command | Purpose |
|
|
281
289
|
|---|---|
|
|
282
|
-
| `vise run-sensors [path]` | Run detected project
|
|
290
|
+
| `vise run-sensors [path]` | Run detected project scripts/wrappers (npm scripts, Gradle, Flutter, lint, typecheck, SDK import smokes); inspect with `--dry-run` before running in an untrusted project |
|
|
283
291
|
| `vise run-sensors [path] --dry-run` | List what would run without executing |
|
|
284
292
|
|
|
285
293
|
### Troubleshooting quick loop
|
|
@@ -335,7 +343,7 @@ MCP-capable hosts can call Vise as structured tool calls instead of shell comman
|
|
|
335
343
|
|
|
336
344
|
### Tool names (snake_case per MCP convention)
|
|
337
345
|
|
|
338
|
-
`inspect_project`, `plan_harness`, `plan_integration`, `init_compliance`, `check_compliance`, `sync_compliance`, `attest_rule`, `explain_rule`, `init_engagement`, `show_engagement`, `search_docs`, `get_doc_page`, `debug_issue`, `validate_setup`, `run_sensors`.
|
|
346
|
+
`inspect_project`, `plan_harness`, `plan_integration`, `init_compliance`, `check_compliance`, `sync_compliance`, `attest_rule`, `explain_rule`, `init_engagement`, `show_engagement`, `resolve_request`, `search_docs`, `get_doc_page`, `debug_issue`, `validate_setup`, `run_sensors`, `suggest_patch`, `design_extract`, `design_check`, `design_preview`, `design_reference`, `design_init_tokens`.
|
|
339
347
|
|
|
340
348
|
These are the same operations as the CLI commands above, exposed as MCP tools.
|
|
341
349
|
|
|
@@ -388,6 +396,8 @@ After `vise init`, your project gets a `sp-vise/` directory. These files become
|
|
|
388
396
|
| `sp-vise/compliance.json` | `vise init` | The rules selected for this integration, the Vise version, the ruleset digest, the target app surface, and an optional engagement link. |
|
|
389
397
|
| `sp-vise/attestations/*.json` | `vise sync` (deterministic) or `vise attest` (host-agent / human) | Per-rule evidence: signer, confidence, rationale, cited files (with source fingerprints for drift detection). |
|
|
390
398
|
| `sp-vise/inspection.json` | `vise init` | The platform, monorepo surface, and design-token signals detected at init time. |
|
|
399
|
+
| `sp-vise/design-contract.json` | `vise design extract` | The extracted design contract: declared tokens, breakpoints, advisory components, source file digests (for freshness detection), and a stable digest over design facts. |
|
|
400
|
+
| `sp-vise/design-reference.html` | `vise design reference` | Self-contained HTML design-system spec (token swatches, type samples, components). Human/VLM-readable; open in a browser alongside the app. |
|
|
391
401
|
| `sp-vise/engagement.json` | `vise engagement init` (optional) | Contractual scope: tier, customer ID, contracted outcomes, reviewer assignment. |
|
|
392
402
|
|
|
393
403
|
**Commit `sp-vise/` to your repo.** `vise check` re-validates against the recorded contract on every run, comparing current code against the recorded attestations. If code changes and breaks a rule, the next `check` reports `deterministic-fail`, `attestation-needed`, or `blocked` — never a silent regression.
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -41,6 +41,10 @@ const tools = new Map([
|
|
|
41
41
|
designInitTokensTool,
|
|
42
42
|
].map((tool) => [tool.name, tool]));
|
|
43
43
|
const bundledSkillName = "social-plus-vise";
|
|
44
|
+
// Pre-rebrand `install-skill` runs created skill dirs/files under this name. We
|
|
45
|
+
// supersede (remove) them on install so a host doesn't keep serving stale
|
|
46
|
+
// foundry/spf guidance alongside the current skill.
|
|
47
|
+
const legacySkillName = "social-plus-foundry";
|
|
44
48
|
const cliResult = await handleCli(process.argv.slice(2));
|
|
45
49
|
if (cliResult === "exit") {
|
|
46
50
|
process.exitCode = process.exitCode ?? 0;
|
|
@@ -608,6 +612,7 @@ async function installSkill(args) {
|
|
|
608
612
|
}
|
|
609
613
|
const destination = skillInstallDestination(args);
|
|
610
614
|
const force = hasFlag(args, "force");
|
|
615
|
+
const supersededLegacy = await removeLegacySkillDir(destination);
|
|
611
616
|
const installedFiles = await copyDirectory(source, destination, force);
|
|
612
617
|
return {
|
|
613
618
|
status: installedFiles.length > 0 ? "installed" : "already-current",
|
|
@@ -616,13 +621,36 @@ async function installSkill(args) {
|
|
|
616
621
|
destination,
|
|
617
622
|
force,
|
|
618
623
|
installedFiles,
|
|
624
|
+
supersededLegacy,
|
|
619
625
|
nextStep: "Restart or reload the host AI coding tool so it discovers the installed skill.",
|
|
620
626
|
};
|
|
621
627
|
}
|
|
628
|
+
// Supersede a pre-rebrand skill install: a prior `install-skill` under the old
|
|
629
|
+
// package name created a sibling `<skillsRoot>/social-plus-foundry/` directory. Left
|
|
630
|
+
// in place, the host keeps offering stale spf/foundry guidance next to the current
|
|
631
|
+
// skill. Remove it on install — but only when it actually looks like a skill dir
|
|
632
|
+
// (contains SKILL.md) and is not the directory we're installing into.
|
|
633
|
+
async function removeLegacySkillDir(destination) {
|
|
634
|
+
const legacyDir = path.join(path.dirname(destination), legacySkillName);
|
|
635
|
+
if (legacyDir === destination) {
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
if (!(await fileExists(path.join(legacyDir, "SKILL.md")))) {
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
await rm(legacyDir, { recursive: true, force: true });
|
|
642
|
+
return [legacyDir];
|
|
643
|
+
}
|
|
622
644
|
async function installInstructionFile(target, args) {
|
|
623
645
|
const force = hasFlag(args, "force");
|
|
624
646
|
const source = path.join(skillSourceDir(), "SKILL.md");
|
|
625
647
|
const content = await readFile(source, "utf8");
|
|
648
|
+
const legacyRule = path.join(path.dirname(target.destination), `${legacySkillName}.mdc`);
|
|
649
|
+
const supersededLegacy = [];
|
|
650
|
+
if (legacyRule !== target.destination && (await fileExists(legacyRule))) {
|
|
651
|
+
await rm(legacyRule, { force: true });
|
|
652
|
+
supersededLegacy.push(legacyRule);
|
|
653
|
+
}
|
|
626
654
|
if (await fileExists(target.destination)) {
|
|
627
655
|
const existing = await readFile(target.destination, "utf8");
|
|
628
656
|
if (existing === content) {
|
|
@@ -634,6 +662,7 @@ async function installInstructionFile(target, args) {
|
|
|
634
662
|
destination: target.destination,
|
|
635
663
|
force,
|
|
636
664
|
installedFiles: [],
|
|
665
|
+
supersededLegacy,
|
|
637
666
|
nextStep: "Restart or reload the host AI coding tool so it discovers the updated project instructions.",
|
|
638
667
|
};
|
|
639
668
|
}
|
|
@@ -651,6 +680,7 @@ async function installInstructionFile(target, args) {
|
|
|
651
680
|
destination: target.destination,
|
|
652
681
|
force,
|
|
653
682
|
installedFiles: [target.destination],
|
|
683
|
+
supersededLegacy,
|
|
654
684
|
nextStep: "Restart or reload the host AI coding tool so it discovers the updated project instructions.",
|
|
655
685
|
};
|
|
656
686
|
}
|
|
@@ -891,7 +921,24 @@ function optionalNumberFlag(args, name) {
|
|
|
891
921
|
return number;
|
|
892
922
|
}
|
|
893
923
|
function hasFlag(args, name) {
|
|
894
|
-
|
|
924
|
+
const exact = `--${name}`;
|
|
925
|
+
const equalsPrefix = `${exact}=`;
|
|
926
|
+
for (const arg of args) {
|
|
927
|
+
if (arg === exact) {
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
if (arg.startsWith(equalsPrefix)) {
|
|
931
|
+
const raw = arg.slice(equalsPrefix.length).trim().toLowerCase();
|
|
932
|
+
if (raw === "" || raw === "true" || raw === "1" || raw === "yes") {
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
if (raw === "false" || raw === "0" || raw === "no") {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
throw new Error(`--${name} must be a boolean when provided with "=".`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return false;
|
|
895
942
|
}
|
|
896
943
|
function keyValueFlag(args, name) {
|
|
897
944
|
const pairs = flagValues(args, name);
|
package/dist/tools/ast.js
CHANGED
|
@@ -10,10 +10,46 @@
|
|
|
10
10
|
* Scope: Single-file, single-step identifier resolution only.
|
|
11
11
|
* No cross-file imports, no type inference, no function boundary traversal.
|
|
12
12
|
*/
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
15
|
+
// Lazily and defensively load the tree-sitter native bindings. tree-sitter ships
|
|
16
|
+
// prebuilt binaries for common platforms (darwin, linux-x64, win32-x64); on others
|
|
17
|
+
// (linux-arm64, Alpine/musl, win32-arm64) the binding can fail to load when no C++
|
|
18
|
+
// toolchain is present. AST is an ADDITIVE layer over the regex validators, so a
|
|
19
|
+
// load failure must degrade to regex-only — NOT take down the entire CLI (including
|
|
20
|
+
// doc-search/compliance commands that never touch a parser) at import time. Static
|
|
21
|
+
// top-level imports would throw at module load and brick every command; this loader
|
|
22
|
+
// confines the failure to the AST path. `undefined` = not yet attempted; `null` =
|
|
23
|
+
// attempted and unavailable.
|
|
24
|
+
let nativeBindings;
|
|
25
|
+
function loadNativeBindings() {
|
|
26
|
+
if (nativeBindings !== undefined)
|
|
27
|
+
return nativeBindings;
|
|
28
|
+
try {
|
|
29
|
+
const ParserCtor = nodeRequire("tree-sitter");
|
|
30
|
+
const tsGrammars = nodeRequire("tree-sitter-typescript");
|
|
31
|
+
const kotlinGrammar = nodeRequire("tree-sitter-kotlin");
|
|
32
|
+
nativeBindings = {
|
|
33
|
+
Parser: ParserCtor,
|
|
34
|
+
tsGrammar: tsGrammars.typescript,
|
|
35
|
+
tsxGrammar: tsGrammars.tsx,
|
|
36
|
+
kotlinGrammar,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
nativeBindings = null;
|
|
41
|
+
}
|
|
42
|
+
return nativeBindings;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether tree-sitter native bindings are available in this environment. When
|
|
46
|
+
* false, every AST helper degrades gracefully: parse() throws (so tryParse()
|
|
47
|
+
* returns null and stripComments() returns the source unchanged), and validators
|
|
48
|
+
* fall back to their regex paths.
|
|
49
|
+
*/
|
|
50
|
+
export function astAvailable() {
|
|
51
|
+
return loadNativeBindings() !== null;
|
|
52
|
+
}
|
|
17
53
|
/**
|
|
18
54
|
* Strip comments from source code using tree-sitter AST.
|
|
19
55
|
* Replaces comment spans with whitespace (preserving line structure).
|
|
@@ -56,13 +92,17 @@ const parsers = new Map();
|
|
|
56
92
|
function getParser(language) {
|
|
57
93
|
let parser = parsers.get(language);
|
|
58
94
|
if (!parser) {
|
|
59
|
-
|
|
95
|
+
const native = loadNativeBindings();
|
|
96
|
+
if (!native) {
|
|
97
|
+
throw new Error("tree-sitter native bindings unavailable; AST analysis disabled (regex fallback in effect)");
|
|
98
|
+
}
|
|
99
|
+
parser = new native.Parser();
|
|
60
100
|
if (language === "tsx")
|
|
61
|
-
parser.setLanguage(tsxGrammar);
|
|
101
|
+
parser.setLanguage(native.tsxGrammar);
|
|
62
102
|
else if (language === "kotlin")
|
|
63
|
-
parser.setLanguage(
|
|
103
|
+
parser.setLanguage(native.kotlinGrammar);
|
|
64
104
|
else
|
|
65
|
-
parser.setLanguage(tsGrammar);
|
|
105
|
+
parser.setLanguage(native.tsGrammar);
|
|
66
106
|
parsers.set(language, parser);
|
|
67
107
|
}
|
|
68
108
|
return parser;
|
package/dist/tools/compliance.js
CHANGED
|
@@ -188,6 +188,7 @@ export async function initEngagement(args) {
|
|
|
188
188
|
: undefined;
|
|
189
189
|
const engagement = {
|
|
190
190
|
schema_version: schemaVersion,
|
|
191
|
+
vise_version: packageVersion,
|
|
191
192
|
foundry_version: packageVersion,
|
|
192
193
|
engagement_id: randomUUID(),
|
|
193
194
|
customer_id: args.customerId,
|
|
@@ -235,6 +236,7 @@ export async function initCompliance(repoPath, request, surfacePath) {
|
|
|
235
236
|
const designContract = await readDesignContract(repoRoot);
|
|
236
237
|
const compliance = {
|
|
237
238
|
schema_version: schemaVersion,
|
|
239
|
+
vise_version: packageVersion,
|
|
238
240
|
foundry_version: packageVersion,
|
|
239
241
|
ruleset_digest: digestJson(refs), // hash of minimal refs (no title)
|
|
240
242
|
generated_at: new Date().toISOString(),
|
|
@@ -688,6 +690,7 @@ function buildAttestation(compliance, rule, signer, confidence, identity, ration
|
|
|
688
690
|
rule_version: rule.version,
|
|
689
691
|
rule_digest: ref.rule_digest,
|
|
690
692
|
ruleset_digest: compliance.ruleset_digest,
|
|
693
|
+
vise_version: packageVersion,
|
|
691
694
|
foundry_version: packageVersion,
|
|
692
695
|
status: signer === "spf-deterministic" ? "deterministic-pass" : "attested",
|
|
693
696
|
signer_claim: {
|
|
@@ -974,7 +977,7 @@ function sidecarReadme(compliance) {
|
|
|
974
977
|
"## Quick start",
|
|
975
978
|
"",
|
|
976
979
|
"1. Read `findings.json` — it contains a snapshot of rule status taken at init time, including any violations found in the current code.",
|
|
977
|
-
"2. Fix the issues listed in `findings.json`, then run `
|
|
980
|
+
"2. Fix the issues listed in `findings.json`, then run `vise check .` to verify.",
|
|
978
981
|
"3. Run `vise sync .` to persist deterministic-pass evidence once rules are green.",
|
|
979
982
|
"4. Run `vise attest . --rule <rule-id> ...` to sign off on intentional implementation decisions.",
|
|
980
983
|
"",
|
package/dist/tools/design.js
CHANGED
|
@@ -1570,6 +1570,7 @@ export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = [
|
|
|
1570
1570
|
const inferredCount = tokens.filter((token) => token.provenance === "inferred").length;
|
|
1571
1571
|
const contract = {
|
|
1572
1572
|
schema_version: DESIGN_CONTRACT_SCHEMA_VERSION,
|
|
1573
|
+
vise_version: packageVersion,
|
|
1573
1574
|
foundry_version: packageVersion,
|
|
1574
1575
|
source: sourceMeta,
|
|
1575
1576
|
digest: "",
|
|
@@ -2322,3 +2323,412 @@ function stableStringify(value) {
|
|
|
2322
2323
|
}
|
|
2323
2324
|
return JSON.stringify(value);
|
|
2324
2325
|
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Structural grounding helper: builds a BriefLine and throws a TypeError at
|
|
2328
|
+
* construction time if groundedIn is empty. This makes the grounding invariant
|
|
2329
|
+
* structural — an ungrounded line is impossible rather than a runtime surprise.
|
|
2330
|
+
*/
|
|
2331
|
+
function line(text, groundedIn, confidence = "high") {
|
|
2332
|
+
if (groundedIn.length === 0) {
|
|
2333
|
+
throw new TypeError(`BriefLine created with empty groundedIn: "${text}"`);
|
|
2334
|
+
}
|
|
2335
|
+
return { text, groundedIn, confidence };
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Per-role keyword rules in spec-defined order.
|
|
2339
|
+
* Each entry: [pattern, role, confidence].
|
|
2340
|
+
* Rules are applied with first-match-wins semantics.
|
|
2341
|
+
* Compound rules (muted+text, muted+bg/surface) must precede their plain
|
|
2342
|
+
* counterparts so "--color-text-muted" resolves to textSecondary, not textPrimary.
|
|
2343
|
+
*/
|
|
2344
|
+
const ROLE_RULES = [
|
|
2345
|
+
// Compound rules — must precede their plain counterparts
|
|
2346
|
+
{
|
|
2347
|
+
test: (n) => /muted/.test(n) && /\btext\b|foreground|fg/.test(n),
|
|
2348
|
+
role: "textSecondary",
|
|
2349
|
+
confidence: "medium",
|
|
2350
|
+
reason: "name contains 'muted' and 'text'/'foreground'/'fg'",
|
|
2351
|
+
},
|
|
2352
|
+
{
|
|
2353
|
+
test: (n) => /muted/.test(n) && /\bbg\b|surface|background/.test(n),
|
|
2354
|
+
role: "surfaceMuted",
|
|
2355
|
+
confidence: "medium",
|
|
2356
|
+
reason: "name contains 'muted' and 'bg'/'surface'/'background'",
|
|
2357
|
+
},
|
|
2358
|
+
// Noun-first compounds — in real design systems the NOUN keyword (text/surface/
|
|
2359
|
+
// bg/border) sets the role family and primary/secondary act as modifiers within
|
|
2360
|
+
// it: "--text-primary" is the primary BODY-TEXT color, not the action color.
|
|
2361
|
+
// These must precede the plain primary/secondary rules below, or first-match-wins
|
|
2362
|
+
// would misbind some of the most common token names in the wild.
|
|
2363
|
+
{
|
|
2364
|
+
test: (n) => /\btext\b|foreground|\bfg\b/.test(n) && /\bprimary\b/.test(n),
|
|
2365
|
+
role: "textPrimary",
|
|
2366
|
+
confidence: "high",
|
|
2367
|
+
reason: "name contains 'text'/'foreground'/'fg' with 'primary' — the noun keyword sets the role family",
|
|
2368
|
+
},
|
|
2369
|
+
{
|
|
2370
|
+
test: (n) => /\btext\b|foreground|\bfg\b/.test(n) && /\bsecondary\b/.test(n),
|
|
2371
|
+
role: "textSecondary",
|
|
2372
|
+
confidence: "high",
|
|
2373
|
+
reason: "name contains 'text'/'foreground'/'fg' with 'secondary' — the noun keyword sets the role family",
|
|
2374
|
+
},
|
|
2375
|
+
{
|
|
2376
|
+
test: (n) => /surface|background|\bbg\b/.test(n) && /\bprimary\b/.test(n),
|
|
2377
|
+
role: "surface",
|
|
2378
|
+
confidence: "high",
|
|
2379
|
+
reason: "name contains 'surface'/'background'/'bg' with 'primary' — a primary surface, not an action color",
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
test: (n) => /surface|background|\bbg\b/.test(n) && /\bsecondary\b/.test(n),
|
|
2383
|
+
role: "surfaceMuted",
|
|
2384
|
+
confidence: "medium",
|
|
2385
|
+
reason: "name contains 'surface'/'background'/'bg' with 'secondary' — a secondary surface",
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
test: (n) => /\bborder\b|\boutline\b|\bdivider\b/.test(n) && (/\bprimary\b/.test(n) || /\bsecondary\b/.test(n)),
|
|
2389
|
+
role: "border",
|
|
2390
|
+
confidence: "medium",
|
|
2391
|
+
reason: "name contains a border keyword with a primary/secondary modifier — the noun keyword sets the role family",
|
|
2392
|
+
},
|
|
2393
|
+
// Primary: plain "primary" → high; "brand"/"accent" → medium
|
|
2394
|
+
{
|
|
2395
|
+
test: (n) => /\bprimary\b/.test(n) && !/brand|accent/.test(n),
|
|
2396
|
+
role: "primaryAction",
|
|
2397
|
+
confidence: "high",
|
|
2398
|
+
reason: "name contains 'primary'",
|
|
2399
|
+
},
|
|
2400
|
+
{
|
|
2401
|
+
test: (n) => /\bbrand\b|\baccent\b/.test(n),
|
|
2402
|
+
role: "primaryAction",
|
|
2403
|
+
confidence: "medium",
|
|
2404
|
+
reason: "name contains 'brand' or 'accent'",
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
test: (n) => /\bsecondary\b/.test(n),
|
|
2408
|
+
role: "secondaryAction",
|
|
2409
|
+
confidence: "high",
|
|
2410
|
+
reason: "name contains 'secondary'",
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
test: (n) => /\bdanger\b|\berror\b|\bdestructive\b/.test(n),
|
|
2414
|
+
role: "danger",
|
|
2415
|
+
confidence: "high",
|
|
2416
|
+
reason: "name contains 'danger', 'error', or 'destructive'",
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
test: (n) => /\bsuccess\b|\bpositive\b/.test(n),
|
|
2420
|
+
role: "success",
|
|
2421
|
+
confidence: "high",
|
|
2422
|
+
reason: "name contains 'success' or 'positive'",
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
test: (n) => /surface|background|\bbg\b/.test(n),
|
|
2426
|
+
role: "surface",
|
|
2427
|
+
confidence: "high",
|
|
2428
|
+
reason: "name contains 'surface', 'background', or 'bg'",
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
test: (n) => /\btext\b|foreground|\bfg\b/.test(n),
|
|
2432
|
+
role: "textPrimary",
|
|
2433
|
+
confidence: "high",
|
|
2434
|
+
reason: "name contains 'text', 'foreground', or 'fg'",
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
test: (n) => /\bborder\b|\boutline\b|\bdivider\b/.test(n),
|
|
2438
|
+
role: "border",
|
|
2439
|
+
confidence: "high",
|
|
2440
|
+
reason: "name contains 'border', 'outline', or 'divider'",
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
test: (n) => /\bfocus\b|\bring\b/.test(n),
|
|
2444
|
+
role: "focus",
|
|
2445
|
+
confidence: "high",
|
|
2446
|
+
reason: "name contains 'focus' or 'ring'",
|
|
2447
|
+
},
|
|
2448
|
+
{
|
|
2449
|
+
test: (n) => /\bavatar\b/.test(n),
|
|
2450
|
+
role: "avatarFallback",
|
|
2451
|
+
confidence: "high",
|
|
2452
|
+
reason: "name contains 'avatar'",
|
|
2453
|
+
},
|
|
2454
|
+
];
|
|
2455
|
+
/** Infer a (role, confidence, reason) from a token name, or null if no rule matches. */
|
|
2456
|
+
function inferRole(tokenName) {
|
|
2457
|
+
const n = tokenName.toLowerCase();
|
|
2458
|
+
for (const rule of ROLE_RULES) {
|
|
2459
|
+
if (rule.test(n)) {
|
|
2460
|
+
return { role: rule.role, confidence: rule.confidence, reason: rule.reason };
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Among multiple candidates for the same role, prefer:
|
|
2467
|
+
* 1. high confidence over medium
|
|
2468
|
+
* 2. shorter name (e.g. "--color-primary" beats "--color-primary-hover")
|
|
2469
|
+
*/
|
|
2470
|
+
function bestCandidate(a, b) {
|
|
2471
|
+
if (a.confidence === "high" && b.confidence !== "high")
|
|
2472
|
+
return a;
|
|
2473
|
+
if (b.confidence === "high" && a.confidence !== "high")
|
|
2474
|
+
return b;
|
|
2475
|
+
return a.token.length <= b.token.length ? a : b;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Build a DesignBuildBrief from a DesignContract.
|
|
2479
|
+
*
|
|
2480
|
+
* - Pure: no I/O, no side effects.
|
|
2481
|
+
* - Never persisted or digested.
|
|
2482
|
+
* - Every BriefLine has non-empty groundedIn citing actual contract tokens/roles.
|
|
2483
|
+
* - Roles inferred from NAME only (never from value).
|
|
2484
|
+
* - Inferred tokens (name: null) are never cited by name; they may be cited only
|
|
2485
|
+
* as a count for do/avoid prose, but only if the contract has declared color
|
|
2486
|
+
* tokens with recognizable names to ground the line instead.
|
|
2487
|
+
*/
|
|
2488
|
+
export function buildDesignBrief(contract) {
|
|
2489
|
+
const strength = contract.stats.strength;
|
|
2490
|
+
// ── Role inference ──────────────────────────────────────────────────────────
|
|
2491
|
+
// Walk only declared color tokens (provenance=declared, category=color, name != null).
|
|
2492
|
+
// Inferred tokens always have name: null; value-only matching is forbidden.
|
|
2493
|
+
const colorTokens = contract.tokens.filter((t) => t.category === "color" && t.name !== null);
|
|
2494
|
+
const roleMap = new Map();
|
|
2495
|
+
for (const token of colorTokens) {
|
|
2496
|
+
const inferred = inferRole(token.name);
|
|
2497
|
+
if (!inferred) {
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
const candidate = {
|
|
2501
|
+
role: inferred.role,
|
|
2502
|
+
token: token.name,
|
|
2503
|
+
value: token.value,
|
|
2504
|
+
confidence: inferred.confidence,
|
|
2505
|
+
reason: inferred.reason,
|
|
2506
|
+
};
|
|
2507
|
+
const existing = roleMap.get(inferred.role);
|
|
2508
|
+
roleMap.set(inferred.role, existing ? bestCandidate(existing, candidate) : candidate);
|
|
2509
|
+
}
|
|
2510
|
+
const roles = [...roleMap.values()];
|
|
2511
|
+
// Helper: is a role name in this brief?
|
|
2512
|
+
const roleNames = new Set(roles.map((r) => r.role));
|
|
2513
|
+
// ── Component hints ─────────────────────────────────────────────────────────
|
|
2514
|
+
// Reference ONLY tokens that actually exist in the contract.
|
|
2515
|
+
const firstToken = (cat) => contract.tokens.find((t) => t.category === cat && t.name !== null);
|
|
2516
|
+
const radiusToken = firstToken("radius");
|
|
2517
|
+
const spaceToken = firstToken("space");
|
|
2518
|
+
// Border colour: sourced from the inferred border role (a color token named *border*/*outline*/*divider*).
|
|
2519
|
+
// There is no "border" TokenCategory — border colours live in the "color" category.
|
|
2520
|
+
const borderRoleColor = roleMap.get("border");
|
|
2521
|
+
const shadowToken = firstToken("shadow");
|
|
2522
|
+
const primaryRole = roleMap.get("primaryAction");
|
|
2523
|
+
// card hint
|
|
2524
|
+
const cardGuidanceLines = [];
|
|
2525
|
+
if (radiusToken) {
|
|
2526
|
+
cardGuidanceLines.push(line(`Use ${radiusToken.name} (${radiusToken.value}) for card corner radius.`, [radiusToken.name]));
|
|
2527
|
+
}
|
|
2528
|
+
if (spaceToken) {
|
|
2529
|
+
cardGuidanceLines.push(line(`Use ${spaceToken.name} (${spaceToken.value}) for card internal padding.`, [spaceToken.name]));
|
|
2530
|
+
}
|
|
2531
|
+
if (borderRoleColor) {
|
|
2532
|
+
cardGuidanceLines.push(line(`Apply ${borderRoleColor.token} for card border colour.`, [borderRoleColor.role]));
|
|
2533
|
+
}
|
|
2534
|
+
else if (shadowToken) {
|
|
2535
|
+
cardGuidanceLines.push(line(`Apply ${shadowToken.name} for card shadow/elevation.`, [shadowToken.name]));
|
|
2536
|
+
}
|
|
2537
|
+
const cardHint = cardGuidanceLines.length > 0
|
|
2538
|
+
? { kind: "card", guidance: cardGuidanceLines, confidence: radiusToken && spaceToken ? "high" : "medium" }
|
|
2539
|
+
: { kind: "card", absent: true, note: "No card pattern confidently identified — reuse the host app's existing card styles." };
|
|
2540
|
+
// button hint
|
|
2541
|
+
const buttonGuidanceLines = [];
|
|
2542
|
+
if (primaryRole) {
|
|
2543
|
+
buttonGuidanceLines.push(line(`Use ${primaryRole.token} (${primaryRole.value}) as the primary button background.`, [primaryRole.role]));
|
|
2544
|
+
}
|
|
2545
|
+
if (radiusToken) {
|
|
2546
|
+
buttonGuidanceLines.push(line(`Apply ${radiusToken.name} (${radiusToken.value}) for button corner radius.`, [radiusToken.name]));
|
|
2547
|
+
}
|
|
2548
|
+
if (spaceToken) {
|
|
2549
|
+
buttonGuidanceLines.push(line(`Apply ${spaceToken.name} (${spaceToken.value}) for button horizontal padding.`, [spaceToken.name]));
|
|
2550
|
+
}
|
|
2551
|
+
const buttonHint = buttonGuidanceLines.length > 0
|
|
2552
|
+
? { kind: "button", guidance: buttonGuidanceLines, confidence: primaryRole ? "high" : "medium" }
|
|
2553
|
+
: { kind: "button", absent: true, note: "No button pattern confidently identified — reuse the host app's existing button styles." };
|
|
2554
|
+
// input hint
|
|
2555
|
+
const inputGuidanceLines = [];
|
|
2556
|
+
if (borderRoleColor) {
|
|
2557
|
+
inputGuidanceLines.push(line(`Use ${borderRoleColor.token} for input border colour.`, [borderRoleColor.role]));
|
|
2558
|
+
}
|
|
2559
|
+
if (radiusToken) {
|
|
2560
|
+
inputGuidanceLines.push(line(`Apply ${radiusToken.name} (${radiusToken.value}) for input corner radius.`, [radiusToken.name]));
|
|
2561
|
+
}
|
|
2562
|
+
if (spaceToken) {
|
|
2563
|
+
inputGuidanceLines.push(line(`Apply ${spaceToken.name} (${spaceToken.value}) for input internal padding.`, [spaceToken.name]));
|
|
2564
|
+
}
|
|
2565
|
+
const inputHint = inputGuidanceLines.length > 0
|
|
2566
|
+
? { kind: "input", guidance: inputGuidanceLines, confidence: borderRoleColor ? "high" : "medium" }
|
|
2567
|
+
: { kind: "input", absent: true, note: "No input pattern confidently identified — reuse the host app's existing input styles." };
|
|
2568
|
+
const componentHints = [cardHint, buttonHint, inputHint];
|
|
2569
|
+
// ── Do/Avoid lines ──────────────────────────────────────────────────────────
|
|
2570
|
+
// Every line MUST be grounded in tokens/roles that actually exist in this brief.
|
|
2571
|
+
const doLines = [];
|
|
2572
|
+
const avoidLines = [];
|
|
2573
|
+
// Only emit do/avoid lines that are grounded in actually-present tokens.
|
|
2574
|
+
const declaredColorTokens = colorTokens.filter((t) => t.provenance === "declared");
|
|
2575
|
+
const declaredSpaceTokens = contract.tokens.filter((t) => t.category === "space" && t.name !== null && t.provenance === "declared");
|
|
2576
|
+
const declaredRadiusTokens = contract.tokens.filter((t) => t.category === "radius" && t.name !== null && t.provenance === "declared");
|
|
2577
|
+
// Do: use declared color tokens
|
|
2578
|
+
if (declaredColorTokens.length > 0) {
|
|
2579
|
+
const tokenNames = declaredColorTokens.slice(0, 3).map((t) => t.name);
|
|
2580
|
+
doLines.push(line(`Reference declared color tokens (e.g. ${tokenNames.join(", ")}) — never introduce new hex literals.`, tokenNames));
|
|
2581
|
+
}
|
|
2582
|
+
// Do: use declared space tokens
|
|
2583
|
+
if (declaredSpaceTokens.length > 0) {
|
|
2584
|
+
const tokenNames = declaredSpaceTokens.slice(0, 3).map((t) => t.name);
|
|
2585
|
+
doLines.push(line(`Reference declared spacing tokens (e.g. ${tokenNames.join(", ")}) for margins, padding, and gaps.`, tokenNames));
|
|
2586
|
+
}
|
|
2587
|
+
// Do: use declared radius tokens
|
|
2588
|
+
if (declaredRadiusTokens.length > 0) {
|
|
2589
|
+
const tokenNames = declaredRadiusTokens.slice(0, 2).map((t) => t.name);
|
|
2590
|
+
doLines.push(line(`Use declared radius tokens (e.g. ${tokenNames.join(", ")}) for corner rounding.`, tokenNames));
|
|
2591
|
+
}
|
|
2592
|
+
// Do: use primary-role token for interactive elements
|
|
2593
|
+
if (primaryRole) {
|
|
2594
|
+
doLines.push(line(`Use the primary colour token (${primaryRole.token}) for primary interactive elements (buttons, CTAs).`, [primaryRole.role]));
|
|
2595
|
+
}
|
|
2596
|
+
// Avoid: hex literals (grounded in declared color tokens)
|
|
2597
|
+
if (declaredColorTokens.length > 0) {
|
|
2598
|
+
const tokenNames = declaredColorTokens.slice(0, 3).map((t) => t.name);
|
|
2599
|
+
avoidLines.push(line(`Do not introduce new hex or colour literals — use the ${declaredColorTokens.length} declared colour token(s) (e.g. ${tokenNames.join(", ")}).`, tokenNames));
|
|
2600
|
+
}
|
|
2601
|
+
// Avoid: raw spacing literals (grounded in declared space tokens)
|
|
2602
|
+
if (declaredSpaceTokens.length > 0) {
|
|
2603
|
+
const tokenNames = declaredSpaceTokens.slice(0, 2).map((t) => t.name);
|
|
2604
|
+
avoidLines.push(line(`Do not hardcode raw spacing values — use declared spacing tokens (e.g. ${tokenNames.join(", ")}).`, tokenNames));
|
|
2605
|
+
}
|
|
2606
|
+
// Avoid: overriding the primary colour token on interactive elements
|
|
2607
|
+
if (primaryRole) {
|
|
2608
|
+
avoidLines.push(line(`Do not override the primary colour token (${primaryRole.token}) with ad-hoc colours on interactive elements.`, [primaryRole.role]));
|
|
2609
|
+
}
|
|
2610
|
+
// ── Review notes ─────────────────────────────────────────────────────────────
|
|
2611
|
+
const reviewNotes = [];
|
|
2612
|
+
if (strength === "weak") {
|
|
2613
|
+
reviewNotes.push("Contract is weak — very few named tokens were found. Guidance above is minimal. Run `vise design extract --from-project` to derive a richer contract from the host project's design system, or provide a prototype.");
|
|
2614
|
+
}
|
|
2615
|
+
if (roles.length === 0) {
|
|
2616
|
+
reviewNotes.push("No colour roles could be inferred from token names. Role-based guidance is unavailable. Ensure tokens use recognisable names (e.g. --color-primary, --color-surface) and run `vise design extract --from-project` again.");
|
|
2617
|
+
}
|
|
2618
|
+
// Suggest missing roles using name examples, not camelCase role identifiers
|
|
2619
|
+
// (camelCase role names must not appear in prose to keep the weak/neutral brief JSON clean).
|
|
2620
|
+
if (!roleNames.has("primaryAction") && contract.stats.declared_tokens > 0) {
|
|
2621
|
+
reviewNotes.push("No primary action colour found — consider naming a token --color-primary (or --color-brand / --color-accent) for primary interactive elements.");
|
|
2622
|
+
}
|
|
2623
|
+
if (!roleNames.has("surface") && contract.stats.declared_tokens > 0) {
|
|
2624
|
+
reviewNotes.push("No surface colour found — consider naming a token --color-surface or --color-background.");
|
|
2625
|
+
}
|
|
2626
|
+
if (!roleNames.has("border") && contract.stats.declared_tokens > 0) {
|
|
2627
|
+
reviewNotes.push("No border colour found — consider naming a token --color-border or --color-outline.");
|
|
2628
|
+
}
|
|
2629
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
2630
|
+
const tokenCount = contract.tokens.filter((t) => t.name !== null).length;
|
|
2631
|
+
const summary = roles.length > 0
|
|
2632
|
+
? `Brief grounded in ${tokenCount} named token(s) and ${roles.length} inferred role(s). Contract strength: ${strength}.`
|
|
2633
|
+
: tokenCount > 0
|
|
2634
|
+
? `Brief grounded in ${tokenCount} named token(s); no colour roles could be inferred from token names. Contract strength: ${strength}.`
|
|
2635
|
+
: `Contract has no named tokens — guidance is unavailable. Contract strength: ${strength}. Run \`vise design extract --from-project\` to derive tokens from the host project.`;
|
|
2636
|
+
return {
|
|
2637
|
+
summary,
|
|
2638
|
+
strength,
|
|
2639
|
+
roles,
|
|
2640
|
+
componentHints,
|
|
2641
|
+
do: doLines,
|
|
2642
|
+
avoid: avoidLines,
|
|
2643
|
+
reviewNotes,
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Build outcome-specific design recipe items grounded in an existing brief.
|
|
2648
|
+
*
|
|
2649
|
+
* HARD INVARIANT: every item is grounded ONLY in roles/tokens already in the brief.
|
|
2650
|
+
* Items for absent roles are silently omitted. Returns `undefined` when zero items
|
|
2651
|
+
* can be grounded (e.g. empty brief).
|
|
2652
|
+
*
|
|
2653
|
+
* Pure — no I/O, no side effects. Generated at plan time; never persisted.
|
|
2654
|
+
*/
|
|
2655
|
+
export function buildOutcomeDesignRecipe(brief, outcome) {
|
|
2656
|
+
const roleMap = new Map(brief.roles.map((r) => [r.role, r]));
|
|
2657
|
+
// Collect groundedIn entries from a given component hint (absent hints contribute nothing).
|
|
2658
|
+
const hintGrounding = (kind) => {
|
|
2659
|
+
const hint = brief.componentHints.find((h) => h.kind === kind);
|
|
2660
|
+
if (!hint || "absent" in hint)
|
|
2661
|
+
return [];
|
|
2662
|
+
return hint.guidance.flatMap((l) => l.groundedIn);
|
|
2663
|
+
};
|
|
2664
|
+
// Collect radius-specific grounding from the card hint by looking for guidance
|
|
2665
|
+
// lines that mention "corner radius" — avoids mis-citing a space token as radius.
|
|
2666
|
+
const cardRadiusGrounding = () => {
|
|
2667
|
+
const hint = brief.componentHints.find((h) => h.kind === "card");
|
|
2668
|
+
if (!hint || "absent" in hint)
|
|
2669
|
+
return [];
|
|
2670
|
+
return hint.guidance
|
|
2671
|
+
.filter((l) => l.text.includes("corner radius"))
|
|
2672
|
+
.flatMap((l) => l.groundedIn);
|
|
2673
|
+
};
|
|
2674
|
+
const items = [];
|
|
2675
|
+
if (outcome === "add-feed") {
|
|
2676
|
+
// Composer / action button — only when primaryAction exists.
|
|
2677
|
+
const primaryAction = roleMap.get("primaryAction");
|
|
2678
|
+
if (primaryAction) {
|
|
2679
|
+
items.push(line(`The post composer action button uses the primary action colour token (${primaryAction.token}).`, ["primaryAction"]));
|
|
2680
|
+
}
|
|
2681
|
+
// Post cards — only when the card hint has grounding tokens.
|
|
2682
|
+
const cardGrounding = hintGrounding("card");
|
|
2683
|
+
if (cardGrounding.length > 0) {
|
|
2684
|
+
items.push(line("Post cards follow the card component hint: apply the card hint tokens for corner radius, padding, and border/shadow.", cardGrounding));
|
|
2685
|
+
}
|
|
2686
|
+
// Post metadata and timestamps.
|
|
2687
|
+
const textSecondary = roleMap.get("textSecondary");
|
|
2688
|
+
if (textSecondary) {
|
|
2689
|
+
items.push(line(`Post metadata and timestamps use the secondary text colour token (${textSecondary.token}).`, ["textSecondary"]));
|
|
2690
|
+
}
|
|
2691
|
+
// Report / delete affordances.
|
|
2692
|
+
const danger = roleMap.get("danger");
|
|
2693
|
+
if (danger) {
|
|
2694
|
+
items.push(line(`Report and delete affordances use the danger colour token (${danger.token}).`, ["danger"]));
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
else {
|
|
2698
|
+
// add-chat
|
|
2699
|
+
// Message bubbles use surface.
|
|
2700
|
+
const surface = roleMap.get("surface");
|
|
2701
|
+
if (surface) {
|
|
2702
|
+
const radiusGrounding = cardRadiusGrounding();
|
|
2703
|
+
if (radiusGrounding.length > 0) {
|
|
2704
|
+
items.push(line(`Message bubbles use the surface colour token (${surface.token}) with the card corner radius token applied.`, ["surface", ...radiusGrounding]));
|
|
2705
|
+
}
|
|
2706
|
+
else {
|
|
2707
|
+
items.push(line(`Message bubbles use the surface colour token (${surface.token}).`, ["surface"]));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
// Own-message vs other-message contrast — ONLY when BOTH primaryAction AND surface exist.
|
|
2711
|
+
const primaryAction = roleMap.get("primaryAction");
|
|
2712
|
+
if (primaryAction && surface) {
|
|
2713
|
+
items.push(line(`Own messages use the primary action colour (${primaryAction.token}) as background; other messages use the surface colour (${surface.token}).`, ["primaryAction", "surface"]));
|
|
2714
|
+
}
|
|
2715
|
+
// Timestamps.
|
|
2716
|
+
const textSecondary = roleMap.get("textSecondary");
|
|
2717
|
+
if (textSecondary) {
|
|
2718
|
+
items.push(line(`Message timestamps use the secondary text colour token (${textSecondary.token}).`, ["textSecondary"]));
|
|
2719
|
+
}
|
|
2720
|
+
// Composer follows the input hint tokens.
|
|
2721
|
+
const inputGrounding = hintGrounding("input");
|
|
2722
|
+
if (inputGrounding.length > 0) {
|
|
2723
|
+
items.push(line("The message composer follows the input component hint: apply the input hint tokens for border colour, corner radius, and padding.", inputGrounding));
|
|
2724
|
+
}
|
|
2725
|
+
// Moderation actions.
|
|
2726
|
+
const danger = roleMap.get("danger");
|
|
2727
|
+
if (danger) {
|
|
2728
|
+
items.push(line(`Moderation actions (report, block, mute) use the danger colour token (${danger.token}).`, ["danger"]));
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
if (items.length === 0)
|
|
2732
|
+
return undefined;
|
|
2733
|
+
return { outcome, items };
|
|
2734
|
+
}
|
package/dist/tools/harness.js
CHANGED
|
@@ -210,7 +210,7 @@ function assessHarnessability(platforms, commandSensors, designSignalCount) {
|
|
|
210
210
|
else {
|
|
211
211
|
gaps.push("No platform signals detected; ask the user for the app framework or repository root.");
|
|
212
212
|
}
|
|
213
|
-
if (platforms.some((platform) => ["typescript", "react-native", "android", "flutter"].includes(platform))) {
|
|
213
|
+
if (platforms.some((platform) => ["typescript", "react-native", "android", "flutter", "ios"].includes(platform))) {
|
|
214
214
|
affordances.push("Detected a platform with deterministic setup checks available in Vise.");
|
|
215
215
|
}
|
|
216
216
|
if (commandSensors.length > 0) {
|
|
@@ -223,7 +223,7 @@ function assessHarnessability(platforms, commandSensors, designSignalCount) {
|
|
|
223
223
|
affordances.push(`Detected ${designSignalCount} design/theme signal(s) for UI integration grounding.`);
|
|
224
224
|
}
|
|
225
225
|
if (platforms.includes("ios")) {
|
|
226
|
-
gaps.push("iOS
|
|
226
|
+
gaps.push("iOS: static compliance rules are fully operational. No build/compile sensor is wired yet (xcodebuild environment requirements make it fragile); run-sensors will return no-sensors for iOS projects.");
|
|
227
227
|
}
|
|
228
228
|
if (platforms.length === 0) {
|
|
229
229
|
return { level: "weak", affordances, gaps };
|
|
@@ -4,7 +4,7 @@ import { BROAD_SOCIAL_REGEX, DESIGN_REGEX, classifyOutcome, getOutcomeDefinition
|
|
|
4
4
|
import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
|
|
5
5
|
import { capabilityChecklist } from "../capabilities.js";
|
|
6
6
|
import { applicableComplianceRuleSummaries } from "./compliance.js";
|
|
7
|
-
import { readDesignContract } from "./design.js";
|
|
7
|
+
import { buildDesignBrief, buildOutcomeDesignRecipe, readDesignContract } from "./design.js";
|
|
8
8
|
import { sdkVersionGuidance } from "./sdkVersion.js";
|
|
9
9
|
import { detectCommandSensors } from "./harness.js";
|
|
10
10
|
import { inspectProject } from "./project.js";
|
|
@@ -71,8 +71,14 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
71
71
|
answers,
|
|
72
72
|
});
|
|
73
73
|
const definition = getOutcomeDefinition(outcome);
|
|
74
|
-
|
|
74
|
+
// Design contract is loaded before intake so the brief can inform the fallback
|
|
75
|
+
// intake question (missing primary-action token) at assembly time.
|
|
75
76
|
const designContract = await readDesignContract(repoRoot);
|
|
77
|
+
const designBrief = designContract ? buildDesignBrief(designContract) : undefined;
|
|
78
|
+
if (designBrief && (outcome === "add-feed" || outcome === "add-chat")) {
|
|
79
|
+
designBrief.outcomeRecipe = buildOutcomeDesignRecipe(designBrief, outcome) ?? undefined;
|
|
80
|
+
}
|
|
81
|
+
const intake = intakeFor(ctx, definition.intakeQuestions(ctx), outcome, designBrief);
|
|
76
82
|
// Advisory SDK-version currency guidance (npm registry for TS/RN; version-agnostic
|
|
77
83
|
// for native). Best-effort — degrades to greenfield "install latest + pin" if the
|
|
78
84
|
// registry is unreachable. Never gates.
|
|
@@ -113,7 +119,7 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
113
119
|
sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
|
|
114
120
|
stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath),
|
|
115
121
|
evidencePolicy: "Every implementation step must cite at least one detected file, docs page, validator rule, or required user input. If evidence is missing, stop and ask the user instead of inventing details.",
|
|
116
|
-
designContract: designContract ? designContractGuidance(designContract) : undefined,
|
|
122
|
+
designContract: designContract && designBrief ? designContractGuidance(designContract, designBrief) : undefined,
|
|
117
123
|
completenessChecklist: completenessChecklistFor(outcome),
|
|
118
124
|
sdkVersion,
|
|
119
125
|
};
|
|
@@ -137,7 +143,7 @@ function completenessChecklistFor(outcome) {
|
|
|
137
143
|
// references `var(--x)` / maps it per platform); inferred tokens carry their
|
|
138
144
|
// raw value plus a usage count and an explicit "inferred" marker so they are
|
|
139
145
|
// never mistaken for authoritative brand values.
|
|
140
|
-
function designContractGuidance(contract) {
|
|
146
|
+
function designContractGuidance(contract, brief) {
|
|
141
147
|
const byCategory = (category) => contract.tokens
|
|
142
148
|
.filter((token) => token.category === category)
|
|
143
149
|
.map((token) => token.provenance === "declared" && token.name
|
|
@@ -162,6 +168,7 @@ function designContractGuidance(contract) {
|
|
|
162
168
|
breakpoints: contract.breakpoints.map((breakpoint) => breakpoint.raw),
|
|
163
169
|
attestation: `When you record a design attestation, cite this contract digest (${contract.digest}) so the generated feed can be claimed conformant to the customer's prototype.`,
|
|
164
170
|
advisoryOnly: "This contract is advisory generation guidance — it adds no deterministic enforcement and never fails `vise check`.",
|
|
171
|
+
brief,
|
|
165
172
|
};
|
|
166
173
|
}
|
|
167
174
|
function intentFor(request, interpretation) {
|
|
@@ -173,7 +180,7 @@ function intentFor(request, interpretation) {
|
|
|
173
180
|
ambiguity: broadSocialRequest || designRequest ? "high" : "medium",
|
|
174
181
|
};
|
|
175
182
|
}
|
|
176
|
-
function intakeFor(ctx, outcomeQuestions) {
|
|
183
|
+
function intakeFor(ctx, outcomeQuestions, outcome, brief) {
|
|
177
184
|
const questions = [...outcomeQuestions];
|
|
178
185
|
if (ctx.mentionsDesign && ctx.designSignals.length === 0 && !hasAnswer(ctx.answers, "design_source")) {
|
|
179
186
|
questions.push({
|
|
@@ -194,6 +201,21 @@ function intakeFor(ctx, outcomeQuestions) {
|
|
|
194
201
|
options: ["yes", "use another source"],
|
|
195
202
|
});
|
|
196
203
|
}
|
|
204
|
+
// Graceful-degradation fallback: when a design contract exists for a feed or chat
|
|
205
|
+
// outcome but no primary-action token was confidently inferred, ask the developer
|
|
206
|
+
// to name the correct token. Non-blocking so it doesn't stall implementation.
|
|
207
|
+
if (brief &&
|
|
208
|
+
(outcome === "add-feed" || outcome === "add-chat") &&
|
|
209
|
+
!brief.roles.some((r) => r.role === "primaryAction") &&
|
|
210
|
+
!hasAnswer(ctx.answers, "primary_action_token")) {
|
|
211
|
+
questions.push({
|
|
212
|
+
id: "primary_action_token",
|
|
213
|
+
question: "Which design token (or color value) should be used as the primary action color? No primary-action token was confidently identified in the design contract.",
|
|
214
|
+
why: "A primary action colour is needed for interactive elements (composer button, own-message bubble). Without a confident token, the agent must guess or omit it.",
|
|
215
|
+
required: false,
|
|
216
|
+
blocksImplementationWhenMissing: false,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
197
219
|
const remainingBlocking = questions.filter((question) => question.blocksImplementationWhenMissing).length;
|
|
198
220
|
return {
|
|
199
221
|
status: remainingBlocking > 0 ? "needs-clarification" : "ready",
|
package/dist/tools/project.js
CHANGED
|
@@ -74,7 +74,7 @@ async function inspectRoot(root) {
|
|
|
74
74
|
}
|
|
75
75
|
// When react-native is detected alongside generic typescript signals, prefer react-native
|
|
76
76
|
// so that platform-specific rules (react-native.*) are used for init/check/run-sensors.
|
|
77
|
-
// Same for android: an agent may create package.json (e.g. to enable
|
|
77
|
+
// Same for android: an agent may create package.json (e.g. to enable a local Vise check script) which
|
|
78
78
|
// would normally trigger typescript detection — suppress it so only android rules apply.
|
|
79
79
|
const rawPlatforms = Array.from(new Set(signals.map((signal) => signal.platform)));
|
|
80
80
|
const hasRN = rawPlatforms.includes("react-native");
|
|
@@ -1118,9 +1118,11 @@ function validateChat(root, platform, sourceContent) {
|
|
|
1118
1118
|
}
|
|
1119
1119
|
}
|
|
1120
1120
|
}
|
|
1121
|
-
// channel-target-resolved: check for hardcoded channelId/conversationId
|
|
1121
|
+
// channel-target-resolved: check for hardcoded channelId/conversationId.
|
|
1122
|
+
// Comment-stripped so a commented-out or documented channelId can't trip this
|
|
1123
|
+
// no-escape (exit-2) gate.
|
|
1122
1124
|
for (const filePath of chatFiles) {
|
|
1123
|
-
const content = sourceContent.get(filePath) ?? "";
|
|
1125
|
+
const content = commentStripped(filePath, platform, sourceContent.get(filePath) ?? "");
|
|
1124
1126
|
const hardcodedChannel = /(?:channelId|conversationId|channel_id)\b[^=\n]*=\s*["'`][a-z0-9-]+["'`]/i.exec(content);
|
|
1125
1127
|
if (hardcodedChannel) {
|
|
1126
1128
|
findings.push(finding(`${platform}.chat.channel-target-resolved`, "error", "Chat code references a hardcoded channelId or conversationId.", relativeFile(root, filePath), "Resolve the channel from user selection, SDK query, or app routing — never hardcode."));
|
|
@@ -1504,7 +1506,7 @@ function validateLiteralGuardrails(root, platform, sourceContent) {
|
|
|
1504
1506
|
/\btargetId\b\s*[:=]\s*["'`]([^"'`]+)["'`]/i,
|
|
1505
1507
|
/\bfeedId\b\s*[:=]\s*["'`]([^"'`]+)["'`]/i,
|
|
1506
1508
|
/\bchannelId\b\s*[:=]\s*["'`]([^"'`]+)["'`]/i,
|
|
1507
|
-
]);
|
|
1509
|
+
], platform);
|
|
1508
1510
|
if (feedTarget && !isAllowedPlaceholder(feedTarget.value)) {
|
|
1509
1511
|
findings.push(finding(`${platform}.feed.target.literal`, "warning", `A hardcoded feed target literal was found: ${feedTarget.name}.`, relativeFile(root, feedTarget.file), "Do not invent or hardcode communityId, targetId, feedId, or channelId. Ask the user for the target or use an existing app-owned selection/create flow."));
|
|
1510
1512
|
}
|
|
@@ -1531,7 +1533,7 @@ function validateLiteralGuardrails(root, platform, sourceContent) {
|
|
|
1531
1533
|
/\bapi[-_]?key\b\s*[:=][\s\S]{0,200}?(?:process\.env\.[A-Z0-9_]+|import\.meta\.env\.[A-Z0-9_]+)\s*(?:\?\?|\|\|)\s*["'`]([^"'`]+)["'`]/i,
|
|
1532
1534
|
// Ternary fallback: `apiKey = X ? 'literal' : ...` captures the truthy branch.
|
|
1533
1535
|
/\bapi[-_]?key\b\s*[:=][\s\S]{0,200}?\?\s*["'`]([^"'`]+)["'`]\s*:/i,
|
|
1534
|
-
]);
|
|
1536
|
+
], platform);
|
|
1535
1537
|
if (inlineApiKey && !isAllowedPlaceholder(inlineApiKey.value)) {
|
|
1536
1538
|
findings.push(finding(`${platform}.secret.inline-api-key`, "warning", "A social.plus API key appears to be hardcoded in source.", relativeFile(root, inlineApiKey.file), "Use the host app's environment/config pattern instead of committing API keys directly into source files. The literal is still committed even when wrapped in an env-fallback (e.g. `defaultValue:`, `??`, `||`, ternary)."));
|
|
1537
1539
|
}
|
|
@@ -1543,7 +1545,7 @@ function validateLiteralGuardrails(root, platform, sourceContent) {
|
|
|
1543
1545
|
/\buser_id\s*[:=]\s*["'`]([^"'`]+)["'`]/i,
|
|
1544
1546
|
/\.login\s*\(\s*["'`]([^"'`]+)["'`]/i,
|
|
1545
1547
|
/\.login\s*\(\s*userId\s*:\s*["'`]([^"'`]+)["'`]/i,
|
|
1546
|
-
]);
|
|
1548
|
+
], platform);
|
|
1547
1549
|
if (literalUserId && !isAllowedPlaceholder(literalUserId.value)) {
|
|
1548
1550
|
findings.push(finding(`${platform}.auth.no-literal-user-id`, "warning", `A hardcoded user identity literal was found: ${literalUserId.name}.`, relativeFile(root, literalUserId.file), "Do not hardcode a userId in source. Read the authenticated user from the host app's auth state (current session, route param, user-store hook, etc.)."));
|
|
1549
1551
|
}
|
|
@@ -1682,8 +1684,9 @@ function validateLiteralGuardrails(root, platform, sourceContent) {
|
|
|
1682
1684
|
}
|
|
1683
1685
|
return findings;
|
|
1684
1686
|
}
|
|
1685
|
-
function firstLiteralAssignment(contents, patterns) {
|
|
1686
|
-
for (const [file,
|
|
1687
|
+
function firstLiteralAssignment(contents, patterns, platform) {
|
|
1688
|
+
for (const [file, rawContent] of contents) {
|
|
1689
|
+
const content = commentStripped(file, platform, rawContent);
|
|
1687
1690
|
for (const pattern of patterns) {
|
|
1688
1691
|
pattern.lastIndex = 0;
|
|
1689
1692
|
const match = pattern.exec(content);
|
|
@@ -2162,6 +2165,87 @@ function astLanguageForFile(filePath, platform) {
|
|
|
2162
2165
|
}
|
|
2163
2166
|
return undefined;
|
|
2164
2167
|
}
|
|
2168
|
+
// Comment-aware view of a source file for the presence-of-a-bad-literal regex
|
|
2169
|
+
// checks that GATE (channel-target-resolved, inline secrets, literal userId/feed
|
|
2170
|
+
// target). A pattern that appears only in a commented-out or documentation line
|
|
2171
|
+
// must not trip a gate — a hard CI failure on a comment is the worst false positive
|
|
2172
|
+
// a compliance gate can produce. ts/tsx/kotlin use the precise tree-sitter stripper;
|
|
2173
|
+
// Swift/Dart (no grammar wired) use the conservative scanner below. Anything else is
|
|
2174
|
+
// returned unchanged.
|
|
2175
|
+
function commentStripped(filePath, platform, content) {
|
|
2176
|
+
const astLang = astLanguageForFile(filePath, platform);
|
|
2177
|
+
if (astLang)
|
|
2178
|
+
return stripComments(astLang, content);
|
|
2179
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2180
|
+
if (ext === ".swift" || ext === ".dart")
|
|
2181
|
+
return stripLineAndBlockComments(content);
|
|
2182
|
+
return content;
|
|
2183
|
+
}
|
|
2184
|
+
// Conservative comment stripper for languages without a wired tree-sitter grammar
|
|
2185
|
+
// (Swift, Dart). Blanks `//` line comments and `/* */` block comments with spaces,
|
|
2186
|
+
// preserving newlines so offsets/line numbers are unchanged. It tracks single-line
|
|
2187
|
+
// string state ("…" and '…') with escape handling so a `//` inside a string or a URL
|
|
2188
|
+
// ("https://…") is not mistaken for a comment. Critically, it only ever blanks
|
|
2189
|
+
// comment spans — never code or string text — so any mis-classification degrades
|
|
2190
|
+
// toward a residual false-positive (a comment left un-stripped), never a silent
|
|
2191
|
+
// false-negative on a gate. Multi-line/raw strings are not modeled precisely, but the
|
|
2192
|
+
// same fail-toward-firing property holds (worst case: a comment is not stripped).
|
|
2193
|
+
function stripLineAndBlockComments(source) {
|
|
2194
|
+
const out = source.split("");
|
|
2195
|
+
let inString = null;
|
|
2196
|
+
let inBlock = false;
|
|
2197
|
+
let i = 0;
|
|
2198
|
+
while (i < source.length) {
|
|
2199
|
+
const c = source[i];
|
|
2200
|
+
const next = source[i + 1];
|
|
2201
|
+
if (inBlock) {
|
|
2202
|
+
if (c === "*" && next === "/") {
|
|
2203
|
+
out[i] = " ";
|
|
2204
|
+
out[i + 1] = " ";
|
|
2205
|
+
i += 2;
|
|
2206
|
+
inBlock = false;
|
|
2207
|
+
continue;
|
|
2208
|
+
}
|
|
2209
|
+
if (c !== "\n")
|
|
2210
|
+
out[i] = " ";
|
|
2211
|
+
i += 1;
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
if (inString) {
|
|
2215
|
+
// Escape: skip the next char — but never jump past a newline, so a stray
|
|
2216
|
+
// trailing backslash can't swallow the following line of real code.
|
|
2217
|
+
if (c === "\\" && next !== "\n") {
|
|
2218
|
+
i += 2;
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
if (c === inString || c === "\n")
|
|
2222
|
+
inString = null;
|
|
2223
|
+
i += 1;
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
if (c === '"' || c === "'") {
|
|
2227
|
+
inString = c;
|
|
2228
|
+
i += 1;
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
if (c === "/" && next === "/") {
|
|
2232
|
+
for (let j = i; j < source.length && source[j] !== "\n"; j += 1)
|
|
2233
|
+
out[j] = " ";
|
|
2234
|
+
while (i < source.length && source[i] !== "\n")
|
|
2235
|
+
i += 1;
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
if (c === "/" && next === "*") {
|
|
2239
|
+
out[i] = " ";
|
|
2240
|
+
out[i + 1] = " ";
|
|
2241
|
+
i += 2;
|
|
2242
|
+
inBlock = true;
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
i += 1;
|
|
2246
|
+
}
|
|
2247
|
+
return out.join("");
|
|
2248
|
+
}
|
|
2165
2249
|
function validateCommentReferenceTypeEnum(root, platform, sourceContent) {
|
|
2166
2250
|
const findings = [];
|
|
2167
2251
|
// TypeScript/React Native: the SDK types referenceType as the string-literal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amityco/social-plus-vise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -62,9 +62,10 @@
|
|
|
62
62
|
"test:sdk-version": "npm run build && node test/run-sdk-version.mjs",
|
|
63
63
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
64
64
|
"test:e2e-package": "npm run build && node test/run-e2e-package.mjs",
|
|
65
|
-
"validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run pack:check",
|
|
65
|
+
"validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:design-brief && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run pack:check",
|
|
66
66
|
"test:ast": "node test/run-ast-helpers.mjs",
|
|
67
67
|
"test:design-extract": "npm run build && node test/run-design-extract.mjs",
|
|
68
|
+
"test:design-brief": "npm run build && node test/run-design-brief.mjs",
|
|
68
69
|
"test:capabilities": "npm run build && node test/run-capabilities.mjs",
|
|
69
70
|
"test:classify": "npm run build && node test/run-classify.mjs",
|
|
70
71
|
"test:debug": "npm run build && node test/run-debug.mjs",
|