@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 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
- > **Every feature delivered correctly confirmed independently with two different AI coding tools.**
85
- > With Vise, both agents built all 9 social features with no production gaps. Without Vise, 3 out of 9 features had hidden problems that would only surface after users complained.
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. `sp-check` turns that vague awareness into a specific, actionable finding.
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 — `sp-check` runs automatically, agent reads specific findings, fixes them, repeats until green | Does an active feedback loop change the outcome? |
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). Vise's `sp-check` catches these with a specific finding; the rules doc does not.
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
- ### Efficiencyrework sessions needed
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
- Vise delivers all 9 features correctly in a single session. The other conditions leave failing features that require additional sessions to diagnose (the gap isn't visible without `sp-check`) and fix.
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
- <sub>Rework sessions are additional developer-initiated prompts needed after the initial session to diagnose and fix the failing features. Each failing feature typically requires at least one session to identify the gap and one to fix it and that's without the benefit of `sp-check` pointing directly at the problem.</sub>
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
- - **Gate-checked:** Every pass was verified by code inspection — the Vise workspaces contain an actual UI-level ban gate; the pure-MCP workspaces do not. Zero attestation shortcuts.
142
- - **Built from scratch** (greenfield seed)not patching existing code.
143
- - **Three arms run with separate tooling.** The Rules-as-Markdown arm has no `sp-check` tool available — it cannot "cheat" by running the checker.
144
- - **N=1 per cell (Phase 1).** Each agent ran each scenario once. Repeatability seeds on the three most discriminating slices (CM-CHAT, CM-MODERATE, CM-PUSH) are pending. These results should be treated as a strong initial signal, not a statistically settled finding.
145
- - Full per-feature scorecards, agent transcripts, and workspace diffs: [`benchmarks/FINDINGS.html`](benchmarks/FINDINGS.html) · [`benchmarks/RULES_AS_MARKDOWN.html`](benchmarks/RULES_AS_MARKDOWN.html)
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 only mode that reliably delivers all features correctly |
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 | (static rule checks; runtime sensors WIP) |
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. `design_init_tokens` |
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 commands (npm scripts, Gradle, Flutter, lint, typecheck, SDK import smokes); never executes arbitrary shell |
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
- return args.includes(`--${name}`);
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 Parser from "tree-sitter";
14
- import TypeScriptGrammars from "tree-sitter-typescript";
15
- import KotlinGrammar from "tree-sitter-kotlin";
16
- const { typescript: tsGrammar, tsx: tsxGrammar } = TypeScriptGrammars;
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
- parser = new Parser();
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(KotlinGrammar);
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;
@@ -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 `npm run sp-check` (or `vise check .` if vise is on PATH) to verify.",
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
  "",
@@ -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
+ }
@@ -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 support is guided until deterministic validators are expanded.");
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
- const intake = intakeFor(ctx, definition.intakeQuestions(ctx));
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",
@@ -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 npm run sp-check) which
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, content] of contents) {
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.12.5",
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",