@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.
- package/CHANGELOG.md +149 -0
- package/README.md +76 -40
- package/dist/outcomes.js +19 -3
- package/dist/server.js +39 -0
- package/dist/tools/compliance.js +68 -20
- package/dist/tools/debug.js +267 -0
- package/dist/tools/harness.js +17 -1
- package/dist/tools/project.js +222 -25
- package/dist/types.js +4 -0
- package/package.json +14 -6
- package/rules/auth.yaml +298 -38
- package/rules/feed.yaml +1 -1
- package/rules/live-data.yaml +316 -36
- package/rules/push.yaml +140 -0
- package/rules/sdk-lifecycle.yaml +1421 -131
- package/rules/security.yaml +60 -0
- package/skills/social-plus-vise/SKILL.md +45 -2
- package/skills/vise-harness-engineer/SKILL.md +35 -0
- package/social.plus-vise.png +0 -0
package/dist/tools/compliance.js
CHANGED
|
@@ -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:
|
|
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(
|
|
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
|
|
282
|
-
const
|
|
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
|
|
310
|
-
|
|
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
|
|
357
|
+
recommendation: finding?.recommendation,
|
|
358
|
+
rationale: rule.rationale,
|
|
336
359
|
current_rule: ruleSummary(rule),
|
|
337
360
|
});
|
|
338
361
|
continue;
|
|
339
362
|
}
|
|
340
|
-
|
|
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
|
|
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:
|
|
383
|
-
reason:
|
|
415
|
+
status: baseStatus,
|
|
416
|
+
reason: fallbackReason,
|
|
384
417
|
finding,
|
|
385
|
-
recommendation: finding
|
|
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
|
-
|
|
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
|
-
"
|
|
902
|
-
"
|
|
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
|
+
};
|
package/dist/tools/harness.js
CHANGED
|
@@ -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
|
|
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) {
|