@amityco/social-plus-vise 0.8.1 → 0.11.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.
@@ -161,7 +161,7 @@ function stringArray(value) {
161
161
  return value.filter((item) => typeof item === "string" && item.trim() !== "");
162
162
  }
163
163
  const validTiers = new Set(["free", "pro", "partner"]);
164
- const validOutcomes = new Set(["setup-sdk", "setup-push", "setup-live-data", "add-feed", "troubleshoot", "validate-setup", "unknown"]);
164
+ const validOutcomes = new Set(["setup-sdk", "setup-push", "setup-live-data", "add-feed", "add-comments", "add-moderation", "add-chat", "troubleshoot", "validate-setup", "unknown"]);
165
165
  export async function initEngagement(args) {
166
166
  const repoRoot = path.resolve(args.repoPath);
167
167
  const engagementFile = engagementPath(repoRoot);
@@ -227,23 +227,35 @@ export async function initCompliance(repoPath, request, surfacePath) {
227
227
  const inspection = await inspectProject(repoRoot, surfacePath);
228
228
  const outcome = classifyOutcome(request);
229
229
  const rules = await applicableRules(outcome, inspection.platforms);
230
- const refs = rules.map(ruleRef);
230
+ const refs = rules.map(ruleRef); // minimal shape — stable digest input
231
+ const fileRefs = rules.map(ruleRefForFile); // adds title for human/agent readers
231
232
  const engagement = await readEngagement(repoRoot);
232
233
  const compliance = {
233
234
  schema_version: schemaVersion,
234
235
  foundry_version: packageVersion,
235
- ruleset_digest: digestJson(refs),
236
+ ruleset_digest: digestJson(refs), // hash of minimal refs (no title)
236
237
  generated_at: new Date().toISOString(),
237
238
  last_synced_at: null,
238
239
  outcome,
239
240
  engagement_id: engagement?.engagement_id,
240
241
  surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
241
- rules: refs,
242
+ rules: fileRefs, // file carries titles
242
243
  };
243
244
  await mkdir(attestationsDir(repoRoot), { recursive: true });
244
245
  await writeJson(compliancePath(repoRoot), compliance);
245
246
  await writeJson(path.join(sidecarDir(repoRoot), "inspection.json"), inspection);
246
247
  await writeFile(path.join(sidecarDir(repoRoot), "README.md"), sidecarReadme(compliance), "utf8");
248
+ // Write a frozen check snapshot so agents can see current rule status immediately
249
+ // without having to run vise check themselves.
250
+ const checkSnapshot = await checkCompliance(repoPath);
251
+ await writeJson(path.join(sidecarDir(repoRoot), "findings.json"), {
252
+ snapshot_at: compliance.generated_at,
253
+ note: "Snapshot taken at vise init time. Re-run `vise check .` after editing code to see current status.",
254
+ outcome: checkSnapshot.outcome,
255
+ status: checkSnapshot.status,
256
+ summary: checkSnapshot.summary,
257
+ rules: checkSnapshot.rules,
258
+ });
247
259
  const warnings = [];
248
260
  if (engagement && engagement.scope.outcomes.length > 0 && !engagement.scope.outcomes.includes(outcome)) {
249
261
  warnings.push(`Outcome "${outcome}" is not in the engagement scope (${engagement.scope.outcomes.join(", ")}). Compliance was still initialized; extend the scope in engagement.json or re-run vise engagement init.`);
@@ -260,7 +272,7 @@ export async function initCompliance(repoPath, request, surfacePath) {
260
272
  };
261
273
  }
262
274
  export async function applicableComplianceRuleSummaries(outcome, platforms) {
263
- return (await applicableRules(outcome, platforms)).map(ruleRef);
275
+ return (await applicableRules(outcome, platforms)).map(ruleRefForFile);
264
276
  }
265
277
  export async function checkCompliance(repoPath) {
266
278
  const repoRoot = path.resolve(repoPath);
@@ -278,8 +290,12 @@ export async function checkCompliance(repoPath) {
278
290
  };
279
291
  }
280
292
  const inspection = await inspectProject(repoRoot, compliance.surface?.path === "." ? undefined : compliance.surface?.path);
281
- const platform = inspection.platforms[0] ?? "unknown";
282
- const findings = await validateSetup(inspection.effectiveRoot, platform);
293
+ const detectedPlatforms = inspection.platforms;
294
+ const recordedPlatforms = compliance.surface?.platforms || [];
295
+ const platformsToValidate = Array.from(new Set([...detectedPlatforms, ...recordedPlatforms]));
296
+ const platforms = platformsToValidate.length > 0 ? platformsToValidate : ["unknown"];
297
+ const allFindings = await Promise.all(platforms.map((p) => validateSetup(inspection.effectiveRoot, p)));
298
+ const findings = allFindings.flat();
283
299
  const findingsById = new Map(findings.map((finding) => [finding.ruleId, finding]));
284
300
  const attestations = await readAttestations(repoRoot);
285
301
  const results = [];
@@ -295,6 +311,7 @@ export async function checkCompliance(repoPath) {
295
311
  // user knows what to provide.
296
312
  const blockersFired = await runBlockers(rule, inspection.effectiveRoot);
297
313
  if (blockersFired.length > 0) {
314
+ const attestable = rule.enforcement.attestation.allowed;
298
315
  results.push({
299
316
  ruleId: rule.id,
300
317
  title: rule.title,
@@ -303,11 +320,16 @@ export async function checkCompliance(repoPath) {
303
320
  reason: blockersFired.map((blocker) => blocker.reason).join(" "),
304
321
  blockers_fired: blockersFired,
305
322
  current_rule: ruleSummary(rule),
323
+ ...(attestable && {
324
+ next_step: `Provide the file(s) listed in blockers_fired, then run \`vise attest . --rule ${rule.id}\` to record your review decision.`,
325
+ }),
306
326
  });
307
327
  continue;
308
328
  }
309
- const finding = deterministicFinding(rule, findingsById);
310
- if (!finding) {
329
+ const hasDeterministicChecks = (rule.enforcement.deterministic ?? []).length > 0;
330
+ const isInferential = !hasDeterministicChecks && !!rule.enforcement.inferential;
331
+ const finding = hasDeterministicChecks ? deterministicFinding(rule, findingsById) : undefined;
332
+ if (hasDeterministicChecks && !finding) {
311
333
  results.push({
312
334
  ruleId: rule.id,
313
335
  title: rule.title,
@@ -332,12 +354,14 @@ export async function checkCompliance(repoPath) {
332
354
  ? "Current deterministic check failed; previously synced deterministic-pass evidence is stale."
333
355
  : "Current deterministic check failed; this rule does not allow attestation.",
334
356
  finding,
335
- recommendation: finding.recommendation,
357
+ recommendation: finding?.recommendation,
358
+ rationale: rule.rationale,
336
359
  current_rule: ruleSummary(rule),
337
360
  });
338
361
  continue;
339
362
  }
340
- const exactMatch = attestation.rule_digest === ref.rule_digest && attestation.ruleset_digest === compliance.ruleset_digest;
363
+ // ruleset_digest is audit metadata; contractDrift above already guarantees the installed ruleset matches compliance.json.
364
+ const exactMatch = attestation.rule_digest === ref.rule_digest;
341
365
  const grandfathered = !exactMatch && isAttestationGrandfathered(rule, attestation);
342
366
  if (exactMatch || grandfathered) {
343
367
  const sourceFingerprintStatus = await checkSourceFingerprints(repoRoot, inspection.effectiveRoot, attestation.source_fingerprints ?? []);
@@ -350,7 +374,8 @@ export async function checkCompliance(repoPath) {
350
374
  status: rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail",
351
375
  reason: "Recorded attestation source fingerprints changed. Re-check the evidence and record a fresh attestation.",
352
376
  finding,
353
- recommendation: finding.recommendation,
377
+ recommendation: finding?.recommendation,
378
+ rationale: rule.rationale,
354
379
  current_rule: ruleSummary(rule),
355
380
  source_fingerprint_status: sourceFingerprintStatus,
356
381
  });
@@ -375,15 +400,24 @@ export async function checkCompliance(repoPath) {
375
400
  continue;
376
401
  }
377
402
  }
403
+ const baseStatus = (rule.enforcement.attestation.allowed || isInferential) ? "attestation-needed" : "deterministic-fail";
404
+ let fallbackReason = "This rule does not allow attestation.";
405
+ if (isInferential) {
406
+ fallbackReason = "Inferential check required. Please provide a host-agent attestation.";
407
+ }
408
+ else if (rule.enforcement.attestation.allowed) {
409
+ fallbackReason = "Deterministic check failed and no valid attestation exists.";
410
+ }
378
411
  results.push({
379
412
  ruleId: rule.id,
380
413
  title: rule.title,
381
414
  severity: rule.severity,
382
- status: rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail",
383
- reason: rule.enforcement.attestation.allowed ? "Deterministic check failed and no valid attestation exists." : "This rule does not allow attestation.",
415
+ status: baseStatus,
416
+ reason: fallbackReason,
384
417
  finding,
385
- recommendation: finding.recommendation,
418
+ recommendation: finding?.recommendation,
386
419
  current_rule: ruleSummary(rule),
420
+ ...(isInferential && { inferential_prompt: rule.enforcement.inferential?.prompt })
387
421
  });
388
422
  }
389
423
  const summary = summarize(results);
@@ -542,7 +576,7 @@ async function loadRuleFiles() {
542
576
  }
543
577
  return loaded;
544
578
  }
545
- async function rulesById() {
579
+ export async function rulesById() {
546
580
  const rules = (await loadRuleFiles()).flatMap((file) => file.rules);
547
581
  return new Map(rules.map((rule) => [rule.id, rule]));
548
582
  }
@@ -554,6 +588,11 @@ function ruleRef(rule) {
554
588
  severity: rule.severity,
555
589
  };
556
590
  }
591
+ // Extends ruleRef with the human-readable title for file output (compliance.json,
592
+ // applicableRules in integration plans). Not used for digest computation.
593
+ function ruleRefForFile(rule) {
594
+ return { ...ruleRef(rule), title: rule.title };
595
+ }
557
596
  function contractDrift(compliance, rules) {
558
597
  const results = [];
559
598
  const refs = compliance.rules.map((ref) => {
@@ -860,8 +899,11 @@ async function readAttestations(repoRoot) {
860
899
  continue;
861
900
  }
862
901
  const attestation = await readJsonIfExists(path.join(dir, entry));
863
- if (attestation?.rule_id) {
864
- result.set(attestation.rule_id, attestation);
902
+ if (attestation?.rule_id && attestation.payload_hash) {
903
+ const { payload_hash, ...withoutHash } = attestation;
904
+ if (digestJson(withoutHash) === payload_hash) {
905
+ result.set(attestation.rule_id, attestation);
906
+ }
865
907
  }
866
908
  }
867
909
  return result;
@@ -898,8 +940,14 @@ function sidecarReadme(compliance) {
898
940
  `- Rules: ${compliance.rules.length}`,
899
941
  `- Generated: ${compliance.generated_at}`,
900
942
  "",
901
- "Run `vise check` to verify current status and `vise sync` to persist deterministic-pass evidence.",
902
- "Host-agent and human attestations include source fingerprints for cited files; `vise check` marks them stale if those files change.",
943
+ "## Quick start",
944
+ "",
945
+ "1. Read `findings.json` — it contains a snapshot of rule status taken at init time, including any violations found in the current code.",
946
+ "2. Fix the issues listed in `findings.json`, then run `npm run sp-check` (or `vise check .` if vise is on PATH) to verify.",
947
+ "3. Run `vise sync .` to persist deterministic-pass evidence once rules are green.",
948
+ "4. Run `vise attest . --rule <rule-id> ...` to sign off on intentional implementation decisions.",
949
+ "",
950
+ "Attestations include source fingerprints; `vise check` marks them stale if the cited files change.",
903
951
  "",
904
952
  ].join("\n");
905
953
  }
@@ -0,0 +1,267 @@
1
+ import { objectInput, optionalBooleanField, stringField, textResult } from "../types.js";
2
+ import { checkCompliance, rulesById } from "./compliance.js";
3
+ import { inspectProject } from "./project.js";
4
+ function sanitizeError(errorMessage) {
5
+ // Strip out local absolute file paths (e.g., /Users/admin/..., /home/user/...)
6
+ let sanitized = errorMessage.replace(/(?:\/(?:users|home)\/[^\s:]+)/gi, "[LOCAL_PATH]");
7
+ // Strip out thread IDs / Hex memory addresses
8
+ sanitized = sanitized.replace(/0x[0-9a-fA-F]+/g, "[HEX_ADDR]");
9
+ return sanitized;
10
+ }
11
+ function extractExceptionClass(errorMessage) {
12
+ // Try to find a Java/Kotlin exception (e.g. java.lang.NullPointerException or com.amity.AmityException)
13
+ const javaMatch = errorMessage.match(/([a-zA-Z0-9_.]+[A-Z][a-zA-Z0-9_]*Exception)/);
14
+ if (javaMatch)
15
+ return javaMatch[1];
16
+ // Try to find a JS Error (e.g. TypeError, Error)
17
+ const jsMatch = errorMessage.match(/([A-Z][a-zA-Z0-9]*Error):/);
18
+ if (jsMatch)
19
+ return jsMatch[1];
20
+ return null;
21
+ }
22
+ export async function debugIssue(repoPath, errorMessage, options = {}) {
23
+ const sanitizedError = sanitizeError(errorMessage);
24
+ const exceptionClass = extractExceptionClass(errorMessage);
25
+ // 1. Inspect Context
26
+ const inspection = await inspectProject(repoPath);
27
+ // 2. Version-mismatch heuristic (NOT used as benchmark evidence).
28
+ // This is a keyword-only heuristic, not real dependency inspection.
29
+ // Version-mismatch scenarios are excluded from the TypeScript pilot
30
+ // (see DEBUGGING_BENCHMARK_PLAN.md Section 5 "Explicitly excluded from the pilot").
31
+ // Do not use this branch as measured benchmark evidence.
32
+ let likelyCause = "";
33
+ if (errorMessage.includes("method not found") || errorMessage.includes("Unresolved reference")) {
34
+ likelyCause = "Potential Version Mismatch: The codebase is using a newer API pattern against an older SDK version installed in the project.";
35
+ }
36
+ // 3. Compliance History Correlation
37
+ const correlatedRules = [];
38
+ let checkResult = null;
39
+ try {
40
+ checkResult = await checkCompliance(repoPath);
41
+ const rulesMap = await rulesById();
42
+ for (const rule of checkResult.rules) {
43
+ if (rule.status === "deterministic-fail" || rule.status === "attestation-needed") {
44
+ // It failed. Check if symptoms match.
45
+ const ruleDef = rulesMap.get(rule.ruleId);
46
+ const symptoms = ruleDef?.symptoms || [];
47
+ if (symptoms.some(s => errorMessage.includes(s))) {
48
+ correlatedRules.push({
49
+ ruleId: rule.ruleId,
50
+ status: "failed",
51
+ impact: `This rule is currently failing and its known symptoms match the crash log.`,
52
+ });
53
+ }
54
+ }
55
+ else if (rule.status === "attested") {
56
+ // Check for false-positive attestations
57
+ const ruleDef = rulesMap.get(rule.ruleId);
58
+ const symptoms = ruleDef?.symptoms || [];
59
+ if (symptoms.some(s => errorMessage.includes(s))) {
60
+ correlatedRules.push({
61
+ ruleId: rule.ruleId,
62
+ status: "attested",
63
+ attestation: rule.attestation,
64
+ impact: `Warning: This rule was attested as passing, but a matching runtime exception was detected. The custom implementation may be flawed.`,
65
+ });
66
+ }
67
+ }
68
+ }
69
+ }
70
+ catch (err) {
71
+ // Ignore error if compliance sidecar doesn't exist yet
72
+ }
73
+ if (!likelyCause && correlatedRules.length > 0) {
74
+ likelyCause = `The crash is likely caused by the non-compliant implementation of ${correlatedRules.map(r => r.ruleId).join(", ")}.`;
75
+ }
76
+ else if (!likelyCause) {
77
+ likelyCause = `An unknown ${exceptionClass || "runtime"} error occurred.`;
78
+ }
79
+ // Build rule-specific remediation guidance from rule definitions and validator findings.
80
+ const suggestedRemediation = buildRemediation(correlatedRules, checkResult);
81
+ const repairBrief = buildRepairBrief(correlatedRules, checkResult, inspection.platforms, likelyCause);
82
+ const payload = {
83
+ correlatedRules,
84
+ likelyCause,
85
+ sanitizedErrorSignature: sanitizedError,
86
+ extractedException: exceptionClass,
87
+ suggestedRemediation,
88
+ repairBrief,
89
+ relevantDocs: [
90
+ {
91
+ title: "Troubleshooting",
92
+ url: "https://learn.social.plus/troubleshooting"
93
+ }
94
+ ]
95
+ };
96
+ if (options.brief) {
97
+ return {
98
+ likelyCause: payload.likelyCause,
99
+ correlatedRules: payload.correlatedRules,
100
+ repairBrief: payload.repairBrief,
101
+ relevantDocs: payload.relevantDocs,
102
+ };
103
+ }
104
+ return payload;
105
+ }
106
+ function buildRemediation(correlatedRules, checkResult) {
107
+ if (correlatedRules.length === 0) {
108
+ return { action: "Review the correlated rules and ensure SDK integration matches the required patterns." };
109
+ }
110
+ const rules = correlatedRules.map(cr => {
111
+ const ruleResult = checkResult?.rules.find(r => r.ruleId === cr.ruleId);
112
+ // Prefer the validator's per-finding recommendation; fall back to a generic note.
113
+ const guidance = ruleResult?.recommendation
114
+ ?? "Review the rule implementation and ensure the SDK integration matches the required pattern.";
115
+ return {
116
+ ruleId: cr.ruleId,
117
+ title: ruleResult?.title ?? cr.ruleId,
118
+ guidance,
119
+ file: ruleResult?.finding?.file,
120
+ docRef: `vise explain ${cr.ruleId}`,
121
+ };
122
+ });
123
+ return {
124
+ action: "Repair the compliance failure(s) identified above.",
125
+ rules,
126
+ };
127
+ }
128
+ function buildRepairBrief(correlatedRules, checkResult, platforms, likelyCause) {
129
+ const primaryRuleId = pickPrimaryRuleId(correlatedRules);
130
+ const verify = verificationCommands(platforms);
131
+ if (!primaryRuleId) {
132
+ return {
133
+ primaryRuleId: null,
134
+ why: likelyCause,
135
+ candidateFiles: [],
136
+ minimumPatchShape: [
137
+ "Inspect the SDK integration path mentioned in the symptom.",
138
+ "Repair only the smallest social.plus code path that explains the runtime failure.",
139
+ ],
140
+ preserve: [
141
+ "Do not widen the patch beyond the failing social.plus integration path.",
142
+ "Preserve any existing compliant setup, auth, and pagination wiring unless the customer explicitly changes behavior.",
143
+ ],
144
+ verify,
145
+ };
146
+ }
147
+ const ruleResult = checkResult?.rules.find((rule) => rule.ruleId === primaryRuleId);
148
+ const template = repairTemplateForRule(primaryRuleId);
149
+ return {
150
+ primaryRuleId,
151
+ why: template.why ?? likelyCause,
152
+ candidateFiles: candidateFilesForRule(primaryRuleId, checkResult),
153
+ minimumPatchShape: template.minimumPatchShape,
154
+ preserve: template.preserve,
155
+ verify,
156
+ };
157
+ }
158
+ function pickPrimaryRuleId(correlatedRules) {
159
+ const failed = correlatedRules.find((rule) => rule.status === "failed");
160
+ return failed?.ruleId ?? correlatedRules[0]?.ruleId ?? null;
161
+ }
162
+ function candidateFilesForRule(ruleId, checkResult) {
163
+ if (!checkResult) {
164
+ return [];
165
+ }
166
+ const matches = checkResult.rules
167
+ .filter((rule) => rule.ruleId === ruleId)
168
+ .flatMap((rule) => {
169
+ const file = rule.finding?.file;
170
+ return typeof file === "string" && file.length > 0 ? [file] : [];
171
+ });
172
+ return Array.from(new Set(matches));
173
+ }
174
+ function verificationCommands(platforms) {
175
+ const commands = [];
176
+ if (platforms.includes("typescript") || platforms.includes("react-native")) {
177
+ commands.push("npm run typecheck");
178
+ }
179
+ commands.push("vise check . --ci");
180
+ commands.push("vise run-sensors .");
181
+ return commands;
182
+ }
183
+ function repairTemplateForRule(ruleId) {
184
+ const templates = {
185
+ "typescript.client.region": {
186
+ why: "The SDK client is initialized without an explicit region or endpoint, so runtime setup does not match the customer's social.plus project.",
187
+ minimumPatchShape: [
188
+ "Edit the existing client setup module or initialization function.",
189
+ "Pass apiRegion or apiEndpoint into the existing client initialization call, such as Client.create() or Client.createClient().",
190
+ "Prefer the app's existing config or environment source if one already exists.",
191
+ ],
192
+ preserve: [
193
+ "Preserve the app's existing config or environment source of truth for region or endpoint.",
194
+ "Do not hardcode a guessed default region if the customer already has a region owner elsewhere in the app.",
195
+ ],
196
+ },
197
+ "typescript.session.renewal": {
198
+ why: "The login path is missing a renewal handler, so the SDK cannot refresh expiring access tokens.",
199
+ minimumPatchShape: [
200
+ "Edit the existing Client.login call in the current login path.",
201
+ "Add sessionHandler.sessionWillRenewAccessToken(renewal).",
202
+ "Call renewal.renew() from that callback.",
203
+ ],
204
+ preserve: [
205
+ "Preserve the existing session owner and login flow shape.",
206
+ "Retain the session handler for the full session lifetime instead of creating a short-lived local object.",
207
+ ],
208
+ },
209
+ "typescript.live-collection.api-mismatch": {
210
+ why: "The feed is using a one-shot list query instead of a reactive LiveCollection, so new posts do not flow into the UI.",
211
+ minimumPatchShape: [
212
+ "Edit the existing feed query function instead of replacing the feed surface.",
213
+ "Convert the one-shot query into a reactive collection with onData.",
214
+ "Return cleanup that unsubscribes the collection.",
215
+ ],
216
+ preserve: [
217
+ "Preserve existing pagination and query wiring such as pageToken, hasMore/loadMore, or infinite-query state.",
218
+ "Do not widen the patch into unrelated rendering or moderation logic while restoring live updates.",
219
+ ],
220
+ },
221
+ "typescript.auth.logout-on-user-switch": {
222
+ why: "The app switches user identity by logging in directly without clearing the previous SDK session.",
223
+ minimumPatchShape: [
224
+ "Edit the existing user-switch flow.",
225
+ "Await Client.logout() before the next Client.login().",
226
+ "Keep the login call's existing renewal handler behavior intact.",
227
+ ],
228
+ preserve: [
229
+ "Preserve the current sessionHandler and renewal path while inserting logout-before-login.",
230
+ "Do not widen the patch into unrelated auth or navigation state.",
231
+ ],
232
+ },
233
+ };
234
+ return (templates[ruleId] ?? {
235
+ why: "A compliance rule correlated strongly with the reported social.plus failure.",
236
+ minimumPatchShape: [
237
+ "Edit the smallest code path associated with the correlated rule.",
238
+ "Bring the implementation back into compliance with the rule requirement.",
239
+ ],
240
+ preserve: [
241
+ "Preserve adjacent compliant wiring while repairing the failing rule.",
242
+ ],
243
+ });
244
+ }
245
+ export const debugIssueTool = {
246
+ name: "debug_issue",
247
+ description: "Debug a runtime crash, build failure, or logic issue against the social.plus SDK's specific context.",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ repoPath: { type: "string" },
252
+ errorMessage: { type: "string" },
253
+ brief: { type: "boolean" },
254
+ },
255
+ required: ["repoPath", "errorMessage"],
256
+ additionalProperties: false,
257
+ },
258
+ async call(input) {
259
+ const args = objectInput(input);
260
+ const repoPath = stringField(args, "repoPath");
261
+ const errorMessage = stringField(args, "errorMessage");
262
+ const result = await debugIssue(repoPath, errorMessage, {
263
+ brief: optionalBooleanField(args, "brief"),
264
+ });
265
+ return textResult(result);
266
+ },
267
+ };
@@ -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) {