@amityco/social-plus-vise 0.8.1 → 0.12.2

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.
@@ -159,25 +159,30 @@ async function loadLocalDocPages(root) {
159
159
  }));
160
160
  }
161
161
  async function fetchText(url) {
162
- const timeoutMs = fetchTimeoutMs();
162
+ return fetchTextWithTimeout(url, "text/plain, text/markdown, */*");
163
+ }
164
+ // Shared network primitive — same AbortController + env-configurable timeout used for
165
+ // docs fetches. Exported so other plan-layer lookups (e.g. the npm registry SDK-version
166
+ // check) reuse one network path rather than adding a parallel one.
167
+ export async function fetchTextWithTimeout(url, accept = "*/*", timeoutMs = fetchTimeoutMs()) {
163
168
  const controller = new AbortController();
164
169
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
165
170
  try {
166
171
  const response = await fetch(url, {
167
172
  headers: {
168
- accept: "text/plain, text/markdown, */*",
173
+ accept,
169
174
  "user-agent": packageUserAgent,
170
175
  },
171
176
  signal: controller.signal,
172
177
  });
173
178
  if (!response.ok) {
174
- throw new Error(`Failed to fetch docs from ${url}: ${response.status} ${response.statusText}`);
179
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
175
180
  }
176
181
  return await response.text();
177
182
  }
178
183
  catch (error) {
179
184
  if (error instanceof Error && error.name === "AbortError") {
180
- throw new Error(`Timed out fetching docs from ${url} after ${timeoutMs}ms.`);
185
+ throw new Error(`Timed out fetching ${url} after ${timeoutMs}ms.`);
181
186
  }
182
187
  throw error;
183
188
  }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { classifyOutcome, getOutcomeDefinition } from "../outcomes.js";
4
4
  import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
5
5
  import { inspectProject } from "./project.js";
6
+ import { rulesById } from "./compliance.js";
6
7
  export function harnessControlsFor(outcome, platforms) {
7
8
  const docsQuery = getOutcomeDefinition(outcome).docsQuery(platforms[0] ?? "sdk");
8
9
  return {
@@ -95,6 +96,19 @@ async function buildHarnessPlan(repoPath, request, surfacePath) {
95
96
  const controls = harnessControlsFor(outcome, inspection.platforms);
96
97
  const commandSensors = await detectCommandSensors(inspection.effectiveRoot, inspection.platforms);
97
98
  const harnessability = assessHarnessability(inspection.platforms, commandSensors, inspection.designSignals.length);
99
+ const rules = await rulesById();
100
+ const feedforward_instructions = [];
101
+ for (const rule of rules.values()) {
102
+ if (rule.feedforward) {
103
+ const outcomeMatch = !rule.applies_when.outcomes || rule.applies_when.outcomes.includes(outcome);
104
+ const platformMatch = !rule.applies_when.platforms || rule.applies_when.platforms.some(p => inspection.platforms.includes(p));
105
+ if (outcomeMatch && platformMatch) {
106
+ feedforward_instructions.push(`[${rule.id}]: ${rule.feedforward}`);
107
+ }
108
+ }
109
+ }
110
+ const steeringLoop = [...controls.steeringLoop];
111
+ steeringLoop.unshift("Fetch and review the capability_matrix_url. If the requested feature is strictly unsupported, short-circuit and emit a 'blocked' status to the user with alternative suggestions.");
98
112
  return {
99
113
  outcome,
100
114
  surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
@@ -103,7 +117,9 @@ async function buildHarnessPlan(repoPath, request, surfacePath) {
103
117
  harnessability,
104
118
  guides: controls.guides,
105
119
  sensors: [...controls.sensors, ...commandSensors],
106
- steeringLoop: controls.steeringLoop,
120
+ steeringLoop,
121
+ capability_matrix_url: process.env.VISE_FEATURE_MATRIX_URL || "https://learn.social.plus/feature-matrix#feature-matrix",
122
+ feedforward_instructions,
107
123
  };
108
124
  }
109
125
  export async function detectCommandSensors(repoPath, platforms) {
@@ -1,8 +1,11 @@
1
- import { access } from "node:fs/promises";
1
+ import { access, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { BROAD_SOCIAL_REGEX, DESIGN_REGEX, classifyOutcome, getOutcomeDefinition, hasAnswer, planContextFor, } from "../outcomes.js";
4
4
  import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
5
+ import { capabilityChecklist } from "../capabilities.js";
5
6
  import { applicableComplianceRuleSummaries } from "./compliance.js";
7
+ import { readDesignContract } from "./design.js";
8
+ import { sdkVersionGuidance } from "./sdkVersion.js";
6
9
  import { detectCommandSensors } from "./harness.js";
7
10
  import { inspectProject } from "./project.js";
8
11
  export const planIntegrationTool = {
@@ -68,24 +71,97 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
68
71
  answers,
69
72
  });
70
73
  const definition = getOutcomeDefinition(outcome);
74
+ const intake = intakeFor(ctx, definition.intakeQuestions(ctx));
75
+ const designContract = await readDesignContract(repoRoot);
76
+ // Advisory SDK-version currency guidance (npm registry for TS/RN; version-agnostic
77
+ // for native). Best-effort — degrades to greenfield "install latest + pin" if the
78
+ // registry is unreachable. Never gates.
79
+ const packageJsonText = await readFile(path.join(root, "package.json"), "utf8").catch(() => undefined);
80
+ const sdkVersion = await sdkVersionGuidance(platform, packageJsonText);
81
+ // Scope decisions, hoisted to the top of the output so they survive a `head`
82
+ // truncation. Blocking questions must be resolved before building; clarifying
83
+ // questions (e.g. which engagement surfaces are in scope) must be decided and
84
+ // the decision stated — so an omitted feature like comments is an explicit,
85
+ // reviewable choice rather than a silent drop. Mirrors the detailed `intake`
86
+ // block below; placed here because the intake block lands past common `head`
87
+ // cut points.
88
+ const decisionsRequired = intake.questions.map((q) => q.blocksImplementationWhenMissing
89
+ ? `[resolve before building] ${q.question}`
90
+ : `[decide & state in your summary] ${q.question}`);
91
+ // Steps-first ordering: implementationSteps + validation lead the object so
92
+ // that an agent piping `vise plan` through `head` still captures the
93
+ // actionable guidance. The verbose scaffolding (intake, docs, surfaces, rule
94
+ // dumps) follows. Do not move implementationSteps back down — feature steps
95
+ // landing past a `head -120` cut is a documented real-world failure mode.
71
96
  return {
72
97
  outcome,
73
98
  platform,
74
- surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
75
- availableSurfaces: inspection.surfaces,
76
99
  supportLevel,
77
100
  intent: intentFor(request, definition.interpretation),
78
- intake: intakeFor(ctx, definition.intakeQuestions(ctx)),
79
- docs: definition.docs(platform).filter((doc) => doc.path !== "unknown"),
101
+ decisionsRequired,
102
+ implementationSteps: definition.implementationSteps(ctx),
103
+ validation: ["validate_setup", "run_sensors", ...definition.validation(platform)],
104
+ nextStep: "After implementing every step above, run `vise check .` and fix findings until green. You are not done until the check passes or each finding is explicitly attested.",
80
105
  requiredInputs: composeRequiredInputs(ctx, definition.requiredInputs(ctx)),
81
106
  targetFiles: await targetFilesFor(root, outcome, platform, inspection.designSignals),
82
107
  implementationRules: composeImplementationRules(ctx, definition.implementationRules(ctx)),
83
- implementationSteps: definition.implementationSteps(ctx),
84
- validation: ["validate_setup", "run_sensors", ...definition.validation(platform)],
108
+ intake,
109
+ docs: definition.docs(platform).filter((doc) => doc.path !== "unknown"),
110
+ surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
111
+ availableSurfaces: inspection.surfaces,
85
112
  applicableRules: await applicableComplianceRuleSummaries(outcome, inspection.platforms),
86
113
  sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
87
114
  stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath),
88
115
  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,
117
+ completenessChecklist: completenessChecklistFor(outcome),
118
+ sdkVersion,
119
+ };
120
+ }
121
+ // Vise-authored capability checklist for the outcome (feed-forward). The agent
122
+ // builds each or opts out with a recorded reason (`// vise: scope-omit <id>
123
+ // <reason>`), which `vise check` reads and reports. Advisory — Vise proposes the
124
+ // set so completeness doesn't depend on the agent remembering it.
125
+ function completenessChecklistFor(outcome) {
126
+ const items = capabilityChecklist(outcome);
127
+ if (items.length === 0) {
128
+ return undefined;
129
+ }
130
+ return {
131
+ note: "Build each capability, or opt out with `// vise: scope-omit <id> <reason>`. `vise check` reports present/missing/opted-out (advisory — never fails the check).",
132
+ capabilities: items,
133
+ };
134
+ }
135
+ // Build advisory UI-generation guidance from an extracted design contract.
136
+ // Declared tokens are surfaced with their custom-property name (so the agent
137
+ // references `var(--x)` / maps it per platform); inferred tokens carry their
138
+ // raw value plus a usage count and an explicit "inferred" marker so they are
139
+ // never mistaken for authoritative brand values.
140
+ function designContractGuidance(contract) {
141
+ const byCategory = (category) => contract.tokens
142
+ .filter((token) => token.category === category)
143
+ .map((token) => token.provenance === "declared" && token.name
144
+ ? `${token.name}: ${token.value}`
145
+ : `${token.value} (inferred, ${token.uses}×)`);
146
+ return {
147
+ digest: contract.digest,
148
+ strength: contract.stats.strength,
149
+ source: contract.source,
150
+ summary: `A design contract was extracted from the customer's prototype (${contract.stats.declared_tokens} declared + ${contract.stats.inferred_tokens} inferred tokens, strength: ${contract.stats.strength}). ` +
151
+ "Build the social.plus UI using these tokens so it matches the prototype's aesthetic. Prefer the declared tokens; treat inferred tokens as advisory and confirm brand values with the customer when the contract is weak.",
152
+ tokens: {
153
+ color: byCategory("color"),
154
+ spacing: byCategory("space"),
155
+ radius: byCategory("radius"),
156
+ shadow: byCategory("shadow"),
157
+ fontFamily: byCategory("fontFamily"),
158
+ fontSize: byCategory("fontSize"),
159
+ motion: byCategory("motion"),
160
+ },
161
+ components: contract.components.map((component) => ({ name: component.name, selector: component.selector })),
162
+ breakpoints: contract.breakpoints.map((breakpoint) => breakpoint.raw),
163
+ 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
+ advisoryOnly: "This contract is advisory generation guidance — it adds no deterministic enforcement and never fails `vise check`.",
89
165
  };
90
166
  }
91
167
  function intentFor(request, interpretation) {