@amityco/social-plus-vise 1.0.0 → 1.1.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 +71 -24
- package/LICENSE +8 -6
- package/README.md +168 -358
- package/dist/capabilities.js +19 -62
- package/dist/intelligence/grounding.js +0 -23
- package/dist/intelligence/placement.js +0 -9
- package/dist/outcomes.js +44 -22
- package/dist/server.js +75 -38
- package/dist/tools/ast.js +3 -209
- package/dist/tools/blocks.js +6 -20
- package/dist/tools/compliance.js +168 -43
- package/dist/tools/creative.js +15 -41
- package/dist/tools/debug.js +0 -16
- package/dist/tools/design.js +18 -364
- package/dist/tools/docs.js +53 -24
- package/dist/tools/experienceCompiler.js +7 -10
- package/dist/tools/experienceSensors.js +1 -1
- package/dist/tools/harness.js +2 -27
- package/dist/tools/integration.js +6 -38
- package/dist/tools/learning.js +1 -1
- package/dist/tools/project.js +763 -546
- package/dist/tools/sdkFacts.js +2 -15
- package/dist/tools/sdkVersion.js +3 -36
- package/dist/tools/sensors.js +0 -6
- package/dist/tools/uxHarness.js +12 -9
- package/package.json +8 -97
- package/rules/chat.yaml +225 -0
- package/rules/event.yaml +45 -0
- package/rules/feed.yaml +24 -24
- package/rules/invitation.yaml +58 -0
- package/rules/live-data.yaml +104 -2
- package/rules/notification-tray.yaml +106 -0
- package/rules/poll.yaml +71 -0
- package/rules/sdk-lifecycle.yaml +112 -6
- package/rules/search.yaml +131 -0
- package/rules/story.yaml +221 -0
- package/rules/user-blocking.yaml +71 -0
- package/sdk-surface/flutter.json +1 -1
- package/sdk-surface/ios.json +1 -1
- package/sdk-surface/manifest.json +12 -12
- package/sdk-surface/models.flutter.json +96 -96
- package/sdk-surface/models.ios.json +1 -1
- package/sdk-surface/typescript.json +4 -4
- package/skills/social-plus-vise/SKILL.md +25 -5
- package/scripts/catalog-coverage-html.mjs +0 -325
- package/scripts/catalog-relationships-html.mjs +0 -686
- package/scripts/catalog-sheets.mjs +0 -286
- package/scripts/dart-model-extractor/bin/extract_models.dart +0 -169
- package/scripts/dart-model-extractor/pubspec.lock +0 -149
- package/scripts/dart-model-extractor/pubspec.yaml +0 -16
- package/scripts/extract-sdk-models.mjs +0 -749
- package/scripts/import-sdk-surface.mjs +0 -161
- package/scripts/pilot-feedback.mjs +0 -107
- package/scripts/workshop-board-html.mjs +0 -1018
- package/scripts/workshop-kit.mjs +0 -252
- package/skills/vise-harness-engineer/SKILL.md +0 -35
package/dist/tools/compliance.js
CHANGED
|
@@ -157,7 +157,7 @@ export const explainRuleTool = {
|
|
|
157
157
|
};
|
|
158
158
|
export const initEngagementTool = {
|
|
159
159
|
name: "init_engagement",
|
|
160
|
-
description: "Initialize sp-vise/engagement.json with customer, tier, and contracted outcome scope. Local-only metadata
|
|
160
|
+
description: "Initialize sp-vise/engagement.json with customer, tier, and contracted outcome scope. Local-only metadata that never leaves your machine.",
|
|
161
161
|
inputSchema: {
|
|
162
162
|
type: "object",
|
|
163
163
|
properties: {
|
|
@@ -255,7 +255,7 @@ export async function initEngagement(args) {
|
|
|
255
255
|
scope: engagement.scope,
|
|
256
256
|
customer_id: engagement.customer_id,
|
|
257
257
|
file: engagementFile,
|
|
258
|
-
note: "Engagement metadata is
|
|
258
|
+
note: "Engagement metadata is recorded locally and never uploaded.",
|
|
259
259
|
};
|
|
260
260
|
}
|
|
261
261
|
export async function showEngagement(repoPath) {
|
|
@@ -317,8 +317,8 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
|
|
|
317
317
|
const supportedOptionalIds = availableOptionalCapabilityIds(capabilityAvailability);
|
|
318
318
|
const selectedOptionalCapabilities = selectedOptionalCapabilityIds(outcome, answers, request, supportedOptionalIds);
|
|
319
319
|
const rules = await applicableRules(outcome, inspection.platforms);
|
|
320
|
-
const refs = rules.map(ruleRef);
|
|
321
|
-
const fileRefs = rules.map(ruleRefForFile);
|
|
320
|
+
const refs = rules.map(ruleRef);
|
|
321
|
+
const fileRefs = rules.map(ruleRefForFile);
|
|
322
322
|
const engagement = await readEngagement(repoRoot);
|
|
323
323
|
const designContract = await readDesignContract(repoRoot);
|
|
324
324
|
const designReview = designReviewFor(repoRoot, designContract, answers);
|
|
@@ -342,25 +342,32 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
|
|
|
342
342
|
allowUnresolvedIntake: options.allowUnresolvedIntake === true,
|
|
343
343
|
});
|
|
344
344
|
if (intake.remainingBlocking > 0 && !intake.acknowledged_unresolved_blocking) {
|
|
345
|
+
const blockingIds = intake.questions
|
|
346
|
+
.filter((question) => question.blocksImplementationWhenMissing)
|
|
347
|
+
.map((question) => question.id);
|
|
348
|
+
const answerExample = blockingIds.map((id) => `${id}=<value>`).join(" --answer ");
|
|
345
349
|
return {
|
|
346
350
|
status: "needs-clarification",
|
|
347
351
|
exitCode: 7,
|
|
348
352
|
outcome,
|
|
349
353
|
intake,
|
|
350
|
-
nextStep: "Run `vise plan` and surface the blocking intake questions to the customer.
|
|
354
|
+
nextStep: "Run `vise plan` and surface the blocking intake questions to the customer. " +
|
|
355
|
+
`Then re-run with their answers (one --answer per id): vise init --request <request> --answer ${answerExample}. ` +
|
|
356
|
+
"Each --answer takes a single id=value; repeat the flag per id (do not comma-join). " +
|
|
357
|
+
"The gate still blocks until every id above is answered; pass --allow-unresolved-intake only for retrospective/harness initialization.",
|
|
351
358
|
};
|
|
352
359
|
}
|
|
353
360
|
const compliance = {
|
|
354
361
|
schema_version: schemaVersion,
|
|
355
362
|
vise_version: packageVersion,
|
|
356
363
|
foundry_version: packageVersion,
|
|
357
|
-
ruleset_digest: digestJson(refs),
|
|
364
|
+
ruleset_digest: digestJson(refs),
|
|
358
365
|
generated_at: new Date().toISOString(),
|
|
359
366
|
last_synced_at: null,
|
|
360
367
|
outcome,
|
|
361
368
|
engagement_id: engagement?.engagement_id,
|
|
362
369
|
surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
|
|
363
|
-
rules: fileRefs,
|
|
370
|
+
rules: fileRefs,
|
|
364
371
|
design_contract: acceptedDesignContract
|
|
365
372
|
? {
|
|
366
373
|
digest: acceptedDesignContract.digest,
|
|
@@ -371,6 +378,7 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
|
|
|
371
378
|
}
|
|
372
379
|
: undefined,
|
|
373
380
|
selected_optional_capabilities: selectedOptionalCapabilities.length > 0 ? selectedOptionalCapabilities : undefined,
|
|
381
|
+
surface_anchor: await buildSurfaceAnchor(rules),
|
|
374
382
|
};
|
|
375
383
|
await mkdir(attestationsDir(repoRoot), { recursive: true });
|
|
376
384
|
await writeJson(compliancePath(repoRoot), compliance);
|
|
@@ -385,8 +393,6 @@ export async function initCompliance(repoPath, request, surfacePath, answers = {
|
|
|
385
393
|
});
|
|
386
394
|
await writeJson(path.join(sidecarDir(repoRoot), "inspection.json"), inspection);
|
|
387
395
|
await writeFile(path.join(sidecarDir(repoRoot), "README.md"), sidecarReadme(compliance), "utf8");
|
|
388
|
-
// Write a frozen check snapshot so agents can see current rule status immediately
|
|
389
|
-
// without having to run vise check themselves.
|
|
390
396
|
const checkSnapshot = await checkCompliance(repoPath);
|
|
391
397
|
await writeJson(path.join(sidecarDir(repoRoot), "findings.json"), {
|
|
392
398
|
snapshot_at: compliance.generated_at,
|
|
@@ -559,7 +565,120 @@ export async function applicableComplianceRuleSummaries(outcome, platforms) {
|
|
|
559
565
|
return (await applicableRules(outcome, platforms)).map(ruleRefForFile);
|
|
560
566
|
}
|
|
561
567
|
export async function applicableCompliancePlanRuleSummaries(outcome, platforms) {
|
|
562
|
-
return (await applicableRules(outcome, platforms)).map(ruleRefForPlan);
|
|
568
|
+
return (await applicableRules(outcome, platforms)).map(ruleRefForPlan).map(planRuleRefSummary);
|
|
569
|
+
}
|
|
570
|
+
const SURFACE_ANCHOR_PLATFORMS = ["typescript", "android", "ios", "flutter"];
|
|
571
|
+
async function loadSurfaceSymbolSets() {
|
|
572
|
+
const { getSdkFacts } = await import("./sdkFacts.js");
|
|
573
|
+
const out = {};
|
|
574
|
+
for (const platform of SURFACE_ANCHOR_PLATFORMS) {
|
|
575
|
+
try {
|
|
576
|
+
const facts = await getSdkFacts({ platform, includeSymbols: true });
|
|
577
|
+
out[platform] = {
|
|
578
|
+
types: new Set((facts.symbols?.types ?? []).map((symbol) => symbol.name)),
|
|
579
|
+
members: new Set((facts.symbols?.members ?? []).map((symbol) => symbol.name)),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
function surfaceIdentityFrom(sets) {
|
|
588
|
+
const shape = {};
|
|
589
|
+
for (const platform of Object.keys(sets).sort()) {
|
|
590
|
+
shape[platform] = { types: [...sets[platform].types].sort(), members: [...sets[platform].members].sort() };
|
|
591
|
+
}
|
|
592
|
+
return digestJson(shape);
|
|
593
|
+
}
|
|
594
|
+
async function buildSurfaceAnchor(ruleDefs) {
|
|
595
|
+
const anchored = ruleDefs
|
|
596
|
+
.filter((rule) => {
|
|
597
|
+
const a = rule.symbol_anchored;
|
|
598
|
+
return !!a && a.basis === "names-only" && ((a.types?.length ?? 0) > 0 || (a.members?.length ?? 0) > 0);
|
|
599
|
+
})
|
|
600
|
+
.map((rule) => ({ rule_id: rule.id, platform: rule.symbol_anchored.platform, types: rule.symbol_anchored.types, members: rule.symbol_anchored.members }));
|
|
601
|
+
if (anchored.length === 0)
|
|
602
|
+
return undefined;
|
|
603
|
+
return { identity: surfaceIdentityFrom(await loadSurfaceSymbolSets()), rules: anchored };
|
|
604
|
+
}
|
|
605
|
+
export async function surfaceDriftStale(compliance) {
|
|
606
|
+
const drifted = new Map();
|
|
607
|
+
const anchor = compliance.surface_anchor;
|
|
608
|
+
if (!anchor || anchor.rules.length === 0)
|
|
609
|
+
return drifted;
|
|
610
|
+
const { canonicalPlatform } = await import("./sdkFacts.js");
|
|
611
|
+
const sets = await loadSurfaceSymbolSets();
|
|
612
|
+
if (surfaceIdentityFrom(sets) === anchor.identity)
|
|
613
|
+
return drifted;
|
|
614
|
+
for (const rec of anchor.rules) {
|
|
615
|
+
const key = canonicalPlatform(rec.platform);
|
|
616
|
+
const surf = key ? sets[key] : undefined;
|
|
617
|
+
if (!surf)
|
|
618
|
+
continue;
|
|
619
|
+
const missingTypes = (rec.types ?? []).filter((type) => !surf.types.has(type));
|
|
620
|
+
const missingMembers = (rec.members ?? []).filter((member) => !surf.members.has(member));
|
|
621
|
+
if (missingTypes.length > 0 || missingMembers.length > 0)
|
|
622
|
+
drifted.set(rec.rule_id, { types: missingTypes, members: missingMembers });
|
|
623
|
+
}
|
|
624
|
+
return drifted;
|
|
625
|
+
}
|
|
626
|
+
export function surfaceDriftReason(rule, missing) {
|
|
627
|
+
const what = [...missing.types.map((type) => `type ${type}`), ...missing.members.map((member) => `member ${member}`)].join(", ");
|
|
628
|
+
const humanReaudit = rule.enforcement.attestation.allowed === false;
|
|
629
|
+
return {
|
|
630
|
+
humanReaudit,
|
|
631
|
+
reason: humanReaudit
|
|
632
|
+
? `SDK surface drifted: this gate's anchored symbol(s) (${what}) are no longer in the bundled SDK surface, and this rule cannot be attested. A maintainer must re-audit it against the current SDK before it can pass again.`
|
|
633
|
+
: `SDK surface drifted: this gate's anchored symbol(s) (${what}) are no longer in the bundled SDK surface. Re-verify the integration and record a fresh attestation.`,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
async function ruleFreshness(rule) {
|
|
637
|
+
const anchor = rule.symbol_anchored;
|
|
638
|
+
if (!anchor) {
|
|
639
|
+
return { status: "not-anchored", note: "This rule does not declare an SDK-symbol anchor." };
|
|
640
|
+
}
|
|
641
|
+
const { getSdkFacts, canonicalPlatform } = await import("./sdkFacts.js");
|
|
642
|
+
const key = canonicalPlatform(anchor.platform);
|
|
643
|
+
let verifiedAgainst;
|
|
644
|
+
if (key) {
|
|
645
|
+
try {
|
|
646
|
+
const facts = await getSdkFacts({ platform: key });
|
|
647
|
+
const upstreamGit = facts.surfaceSource?.upstreamGit;
|
|
648
|
+
verifiedAgainst = {
|
|
649
|
+
platform: anchor.platform,
|
|
650
|
+
sdk_product: facts.sdkProduct ?? null,
|
|
651
|
+
sdk_version: facts.sdkVersion ?? null,
|
|
652
|
+
surface_commit: upstreamGit?.commit ?? null,
|
|
653
|
+
extracted_at: facts.extractedAt ?? null,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const hasSymbols = (anchor.types?.length ?? 0) > 0 || (anchor.members?.length ?? 0) > 0;
|
|
660
|
+
if (anchor.basis !== "names-only" || !hasSymbols) {
|
|
661
|
+
return {
|
|
662
|
+
status: "pattern-based",
|
|
663
|
+
note: "This gate keys on a code pattern/value, not a symbol's existence — not symbol-tracked for drift.",
|
|
664
|
+
anchor,
|
|
665
|
+
...(verifiedAgainst && { verified_against: verifiedAgainst }),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const sets = await loadSurfaceSymbolSets();
|
|
669
|
+
const surf = key ? sets[key] : undefined;
|
|
670
|
+
if (!surf) {
|
|
671
|
+
return { status: "surface-unavailable", anchor, ...(verifiedAgainst && { verified_against: verifiedAgainst }) };
|
|
672
|
+
}
|
|
673
|
+
const missingTypes = (anchor.types ?? []).filter((type) => !surf.types.has(type));
|
|
674
|
+
const missingMembers = (anchor.members ?? []).filter((member) => !surf.members.has(member));
|
|
675
|
+
const drifted = missingTypes.length > 0 || missingMembers.length > 0;
|
|
676
|
+
return {
|
|
677
|
+
status: drifted ? "drifted" : "verified",
|
|
678
|
+
anchor,
|
|
679
|
+
...(drifted && { missing: { types: missingTypes, members: missingMembers } }),
|
|
680
|
+
...(verifiedAgainst && { verified_against: verifiedAgainst }),
|
|
681
|
+
};
|
|
563
682
|
}
|
|
564
683
|
export async function checkCompliance(repoPath) {
|
|
565
684
|
const repoRoot = path.resolve(repoPath);
|
|
@@ -598,13 +717,10 @@ export async function checkCompliance(repoPath) {
|
|
|
598
717
|
results.push({ ...checkRuleIdentity(ref.rule_id), title: ref.rule_id, severity: ref.severity, status: "stale", reason: "Rule is missing from installed Vise." });
|
|
599
718
|
continue;
|
|
600
719
|
}
|
|
601
|
-
// Blockers run first. If any external prerequisite is missing, the rule is
|
|
602
|
-
// "blocked" — the host agent cannot fix this alone, so we surface it and
|
|
603
|
-
// stop evaluating this rule. Customer-readable reasons are attached so the
|
|
604
|
-
// user knows what to provide.
|
|
605
720
|
const blockersFired = await runBlockers(rule, inspection.effectiveRoot);
|
|
606
721
|
if (blockersFired.length > 0) {
|
|
607
722
|
const attestable = rule.enforcement.attestation.allowed;
|
|
723
|
+
const hint = attestable ? attestHint(rule, compliance) : undefined;
|
|
608
724
|
results.push({
|
|
609
725
|
...checkRuleIdentity(rule.id),
|
|
610
726
|
title: rule.title,
|
|
@@ -613,8 +729,9 @@ export async function checkCompliance(repoPath) {
|
|
|
613
729
|
reason: blockersFired.map((blocker) => blocker.reason).join(" "),
|
|
614
730
|
blockers_fired: blockersFired,
|
|
615
731
|
current_rule: ruleSummary(rule),
|
|
616
|
-
...(
|
|
617
|
-
next_step: `Provide the file(s) listed in blockers_fired, then run
|
|
732
|
+
...(hint && {
|
|
733
|
+
next_step: `Provide the file(s) listed in blockers_fired, then run \`${hint.attest_command}\` to record your review decision.`,
|
|
734
|
+
...hint,
|
|
618
735
|
}),
|
|
619
736
|
});
|
|
620
737
|
continue;
|
|
@@ -634,9 +751,6 @@ export async function checkCompliance(repoPath) {
|
|
|
634
751
|
}
|
|
635
752
|
const attestation = attestations.get(rule.id);
|
|
636
753
|
if (attestation) {
|
|
637
|
-
// Deterministic-pass records are historical sync evidence, not waivers.
|
|
638
|
-
// If the current source now produces a finding, the old sync record must
|
|
639
|
-
// not mask code drift; the next `vise sync` will remove it.
|
|
640
754
|
if (attestation.status === "deterministic-pass") {
|
|
641
755
|
const failStatus = rule.advisory ? "advisory" : rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail";
|
|
642
756
|
results.push({
|
|
@@ -657,7 +771,6 @@ export async function checkCompliance(repoPath) {
|
|
|
657
771
|
});
|
|
658
772
|
continue;
|
|
659
773
|
}
|
|
660
|
-
// ruleset_digest is audit metadata; contractDrift above already guarantees the installed ruleset matches compliance.json.
|
|
661
774
|
const exactMatch = attestation.rule_digest === ref.rule_digest;
|
|
662
775
|
const grandfathered = !exactMatch && isAttestationGrandfathered(rule, attestation);
|
|
663
776
|
if (exactMatch || grandfathered) {
|
|
@@ -727,24 +840,28 @@ export async function checkCompliance(repoPath) {
|
|
|
727
840
|
...(baseStatus === "attestation-needed" && rule.enforcement.attestation.allowed && attestHint(rule, compliance)),
|
|
728
841
|
});
|
|
729
842
|
}
|
|
843
|
+
const driftStale = await surfaceDriftStale(compliance);
|
|
844
|
+
if (driftStale.size > 0) {
|
|
845
|
+
for (const result of results) {
|
|
846
|
+
const ruleId = result.contractRuleId ?? result.ruleId;
|
|
847
|
+
const missing = driftStale.get(ruleId);
|
|
848
|
+
if (!missing)
|
|
849
|
+
continue;
|
|
850
|
+
if (result.status !== "deterministic-pass" && result.status !== "attested")
|
|
851
|
+
continue;
|
|
852
|
+
const rule = rules.get(ruleId);
|
|
853
|
+
if (!rule)
|
|
854
|
+
continue;
|
|
855
|
+
result.status = "stale";
|
|
856
|
+
result.reason = surfaceDriftReason(rule, missing).reason;
|
|
857
|
+
result.surface_drift = missing;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
730
860
|
const summary = summarize(results);
|
|
731
861
|
const hasBlocked = results.some((result) => result.status === "blocked");
|
|
732
862
|
const hasDeterministicFailure = results.some((result) => result.status === "deterministic-fail");
|
|
733
|
-
// "advisory" status is intentionally excluded — advisory rules surface but never block.
|
|
734
863
|
const needsAttestation = results.some((result) => result.status === "attestation-needed" || result.status === "stale");
|
|
735
|
-
// Precedence: blocked (3) > deterministic-failures (2) > needs-attestation (1) >
|
|
736
|
-
// completeness-gap (5) > selected-capability-failures (6) > green (0).
|
|
737
|
-
// Contract drift (exit 4) is handled earlier and short-circuits the loop.
|
|
738
|
-
// Completeness-gap: capabilities that are neither present nor validly opted-out require an explicit decision
|
|
739
|
-
// (build it, or place // vise: scope-omit <id> — <reason>). The scope-omit escape hatch keeps this
|
|
740
|
-
// FP-free because any capability can be excluded with a recorded reason. Failure to assess is silently ignored.
|
|
741
864
|
const sourceCompleteness = (await assessProjectCompleteness(inspection.effectiveRoot, compliance.outcome).catch(() => null)) ?? undefined;
|
|
742
|
-
// Installed Block Factory blocks deliver capabilities inside their packages,
|
|
743
|
-
// which the customer-source scan cannot see. Overlay block evidence for
|
|
744
|
-
// still-missing checklist items, but only from sidecar entries that pass
|
|
745
|
-
// registry-free local validation (manifest dependency declared + every
|
|
746
|
-
// filesTouched path intact — see installedBlockProvidedCapabilities). On
|
|
747
|
-
// local drift the evidence is withheld and the normal gap returns.
|
|
748
865
|
const blockProvided = sourceCompleteness && sourceCompleteness.missing.length > 0
|
|
749
866
|
? await installedBlockProvidedCapabilities(repoRoot).catch(() => [])
|
|
750
867
|
: [];
|
|
@@ -752,8 +869,6 @@ export async function checkCompliance(repoPath) {
|
|
|
752
869
|
const hasCompletenessGap = (completeness?.missing.length ?? 0) > 0;
|
|
753
870
|
const selectedOptionalCapabilities = (await assessProjectSelectedOptionalCapabilities(inspection.effectiveRoot, compliance.outcome, compliance.selected_optional_capabilities ?? []).catch(() => null)) ?? undefined;
|
|
754
871
|
const hasSelectedOptionalFailures = ((selectedOptionalCapabilities?.failed.length ?? 0) > 0) || ((selectedOptionalCapabilities?.unknown.length ?? 0) > 0);
|
|
755
|
-
// Blocked wins because the agent cannot proceed without customer input;
|
|
756
|
-
// surfacing a smaller failure first would distract from the real blocker.
|
|
757
872
|
const status = hasBlocked
|
|
758
873
|
? "blocked"
|
|
759
874
|
: hasDeterministicFailure
|
|
@@ -891,7 +1006,7 @@ function fallbackExperienceReport(check) {
|
|
|
891
1006
|
},
|
|
892
1007
|
},
|
|
893
1008
|
score: null,
|
|
894
|
-
nextStep: "Resolve any technical check issue first, then regenerate the report. Do not publish a single Experience Score until repeated
|
|
1009
|
+
nextStep: "Resolve any technical check issue first, then regenerate the report. Do not publish a single Experience Score until repeated benchmark evidence calibrates it responsibly.",
|
|
895
1010
|
};
|
|
896
1011
|
}
|
|
897
1012
|
export async function syncCompliance(repoPath) {
|
|
@@ -949,8 +1064,6 @@ export async function attestRule(args) {
|
|
|
949
1064
|
const ids = candidateIds.length > 0 ? candidateIds : ambiguousCandidates.slice(0, 8);
|
|
950
1065
|
throw new Error(`Rule is ambiguous in this compliance contract: ${args.ruleId}. Attest one contract rule at a time: ${ids.join(", ")}.`);
|
|
951
1066
|
}
|
|
952
|
-
// Collect up to 8 applicable attestable rule ids from this contract for the error hint. Prefer
|
|
953
|
-
// ids that share the bad id's non-wildcard prefix so the agent can narrow down quickly.
|
|
954
1067
|
const attestableIds = compliance.rules
|
|
955
1068
|
.map((ref) => rules.get(ref.rule_id))
|
|
956
1069
|
.filter((r) => r !== undefined && r.enforcement.attestation.allowed);
|
|
@@ -1035,6 +1148,7 @@ export async function explainRule(ruleId) {
|
|
|
1035
1148
|
attestation: rule.enforcement.attestation,
|
|
1036
1149
|
summary: ruleSummary(rule),
|
|
1037
1150
|
rule_digest: digestRule(rule),
|
|
1151
|
+
freshness: await ruleFreshness(rule),
|
|
1038
1152
|
};
|
|
1039
1153
|
}
|
|
1040
1154
|
export async function statusCompliance(repoPath) {
|
|
@@ -1125,8 +1239,6 @@ function ruleRef(rule) {
|
|
|
1125
1239
|
severity: rule.severity,
|
|
1126
1240
|
};
|
|
1127
1241
|
}
|
|
1128
|
-
// Extends ruleRef with the human-readable title for file output (compliance.json,
|
|
1129
|
-
// applicableRules in integration plans). Not used for digest computation.
|
|
1130
1242
|
function ruleRefForFile(rule) {
|
|
1131
1243
|
const publicRuleId = publicProductRuleId(rule.id);
|
|
1132
1244
|
return {
|
|
@@ -1157,8 +1269,14 @@ function ruleRefForPlan(rule) {
|
|
|
1157
1269
|
: undefined,
|
|
1158
1270
|
};
|
|
1159
1271
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1272
|
+
function planRuleRefSummary(ref) {
|
|
1273
|
+
return {
|
|
1274
|
+
rule_id: ref.rule_id,
|
|
1275
|
+
...(ref.title !== undefined ? { title: ref.title } : {}),
|
|
1276
|
+
...(ref.contract_rule_id !== undefined ? { contract_rule_id: ref.contract_rule_id } : {}),
|
|
1277
|
+
...(ref.validator !== undefined ? { validator: ref.validator } : {}),
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1162
1280
|
function attestHint(rule, compliance) {
|
|
1163
1281
|
const minConfidence = rule.enforcement.attestation.host_agent_min_confidence ?? "high";
|
|
1164
1282
|
const fields = rule.enforcement.attestation.evidence_required ?? [];
|
|
@@ -1470,12 +1588,19 @@ function validateEvidence(rule, evidence) {
|
|
|
1470
1588
|
}
|
|
1471
1589
|
async function readCompliance(repoRoot) {
|
|
1472
1590
|
const file = compliancePath(repoRoot);
|
|
1591
|
+
let raw;
|
|
1473
1592
|
try {
|
|
1474
|
-
|
|
1593
|
+
raw = await readFile(file, "utf8");
|
|
1475
1594
|
}
|
|
1476
1595
|
catch {
|
|
1477
1596
|
throw new Error(`No compliance sidecar found. Run vise init first: ${file}`);
|
|
1478
1597
|
}
|
|
1598
|
+
try {
|
|
1599
|
+
return JSON.parse(raw);
|
|
1600
|
+
}
|
|
1601
|
+
catch {
|
|
1602
|
+
throw new Error(`sp-vise/compliance.json is present but could not be parsed (corrupt or merge-conflicted). Re-run vise init to regenerate it: ${file}`);
|
|
1603
|
+
}
|
|
1479
1604
|
}
|
|
1480
1605
|
async function readAttestations(repoRoot) {
|
|
1481
1606
|
const dir = attestationsDir(repoRoot);
|
package/dist/tools/creative.js
CHANGED
|
@@ -9,7 +9,6 @@ import { inspectProject } from "./project.js";
|
|
|
9
9
|
import { buildUxHarness } from "./uxHarness.js";
|
|
10
10
|
import { groundSelection, NO_FIT } from "../intelligence/grounding.js";
|
|
11
11
|
import { SOCIAL_PLUS_OBJECTS, outcomesForObjects, surfaceObjectIds, SURFACE_REGISTRY } from "../intelligence/placement.js";
|
|
12
|
-
// Re-exported so existing importers (e.g. test/run-intelligence-catalog.mjs) keep their path.
|
|
13
12
|
export { SOCIAL_PLUS_OBJECTS };
|
|
14
13
|
const CREATIVE_SCHEMA_VERSION = "2026-06-06.vise-creative.v1";
|
|
15
14
|
const CREATIVE_SELECTION_SCHEMA_VERSION = "2026-06-06.vise-creative-selection.v1";
|
|
@@ -68,7 +67,7 @@ export const creativeBriefTool = {
|
|
|
68
67
|
},
|
|
69
68
|
rankingPreview: {
|
|
70
69
|
type: "boolean",
|
|
71
|
-
description: "Explicit opt-in local preview of
|
|
70
|
+
description: "Explicit opt-in local preview of the v2-draft candidate ranking. Defaults to false and does not change the default candidate order.",
|
|
72
71
|
},
|
|
73
72
|
write: {
|
|
74
73
|
type: "boolean",
|
|
@@ -133,8 +132,6 @@ export const creativeAcceptTool = {
|
|
|
133
132
|
const args = objectInput(input);
|
|
134
133
|
const variantId = stringField(args, "variantId");
|
|
135
134
|
const closest = optionalStringField(args, "closest");
|
|
136
|
-
// Divert "none" to the no-fit recorder ABOVE acceptCreativeVariant's candidate lookup (which would
|
|
137
|
-
// otherwise throw "not found" before grounding runs). acceptCreativeVariant stays untouched.
|
|
138
135
|
if (variantId.trim().toLowerCase() === NO_FIT) {
|
|
139
136
|
return textResult(await recordCatalogGap({
|
|
140
137
|
repoPath: stringField(args, "repoPath"),
|
|
@@ -146,7 +143,6 @@ export const creativeAcceptTool = {
|
|
|
146
143
|
write: optionalBooleanField(args, "write", true),
|
|
147
144
|
}));
|
|
148
145
|
}
|
|
149
|
-
// `closest` only describes a no-fit's nearest variant; fail loud rather than silently ignore it.
|
|
150
146
|
if (closest !== undefined) {
|
|
151
147
|
throw new Error('`closest` only applies to a no-fit record (--variant none); omit it when accepting a variant.');
|
|
152
148
|
}
|
|
@@ -199,7 +195,7 @@ export async function buildCreativeBrief(options) {
|
|
|
199
195
|
candidateSolutions: candidates,
|
|
200
196
|
selectionGuidance: {
|
|
201
197
|
mode: "agent-grounded-selection",
|
|
202
|
-
instruction: "You (the driving agent) choose the best-fitting variant by reasoning over candidateSolutions and the request. Use each candidate's `description` and `whenToChoose` (self-describing selecting signals) to discriminate between close variants — match the signals present in the request to the variant whose `whenToChoose` they satisfy. The listed order and the `fallbackDefault` flag are advisory keyword-derived hints, not recommendations to follow — keyword matching can point at the wrong variant for off-keyword requests, so decide by semantic fit.
|
|
198
|
+
instruction: "You (the driving agent) choose the best-fitting variant by reasoning over candidateSolutions and the request. Use each candidate's `description` and `whenToChoose` (self-describing selecting signals) to discriminate between close variants — match the signals present in the request to the variant whose `whenToChoose` they satisfy. The listed order and the `fallbackDefault` flag are advisory keyword-derived hints, not recommendations to follow — keyword matching can point at the wrong variant for off-keyword requests, so decide by semantic fit. Vise will validate (ground) your choice against the catalog; it does not infer the variant for you.",
|
|
203
199
|
howToAccept: "vise creative accept <repoPath> --variant <candidate id> --rationale \"<one line, grounded in the request and catalog>\" --confidence high|medium|low",
|
|
204
200
|
contract: {
|
|
205
201
|
variant: "A candidateSolutions id chosen by best fit to the request (not by listed order or the fallbackDefault flag).",
|
|
@@ -219,7 +215,7 @@ export async function buildCreativeBrief(options) {
|
|
|
219
215
|
options: candidates.map((candidate) => candidate.id),
|
|
220
216
|
},
|
|
221
217
|
],
|
|
222
|
-
nextStep: "Choose the best-fitting variant for the request by reasoning over candidateSolutions (listed order is advisory), then accept it with a grounded rationale and confidence: `vise creative accept <repoPath> --variant <id> --rationale \"<why>\" --confidence high|medium|low`.
|
|
218
|
+
nextStep: "Choose the best-fitting variant for the request by reasoning over candidateSolutions (listed order is advisory), then accept it with a grounded rationale and confidence: `vise creative accept <repoPath> --variant <id> --rationale \"<why>\" --confidence high|medium|low`. Vise validates the selection against the catalog. If no candidate genuinely fits, record a no-fit instead of forcing a choice: `vise creative accept <repoPath> --variant none --rationale \"<what is missing>\"`.",
|
|
223
219
|
};
|
|
224
220
|
if (options.rankingPreview === true) {
|
|
225
221
|
brief.candidateRankingPreview = buildCandidateRankingPreview(brief);
|
|
@@ -246,9 +242,6 @@ export async function acceptCreativeVariant(options) {
|
|
|
246
242
|
if (!selected) {
|
|
247
243
|
throw new Error(`Creative variant "${variantId}" was not found in ${repoRelativePath(repoRoot, briefPath)}. Available variants: ${brief.candidateSolutions.map((candidate) => candidate.id).join(", ") || "(none)"}.`);
|
|
248
244
|
}
|
|
249
|
-
// Option A: if the driving agent supplied a grounded selection, deterministically validate it against
|
|
250
|
-
// the candidate set the brief presented. A rejected selection (hallucinated id, no rationale, invalid
|
|
251
|
-
// confidence) is refused; needs-review signals (low/missing confidence) are recorded, not blocked.
|
|
252
245
|
let grounding;
|
|
253
246
|
if (options.rationale !== undefined || options.confidence !== undefined) {
|
|
254
247
|
grounding = groundSelection({ variantId, rationale: options.rationale, confidence: options.confidence }, brief.candidateSolutions.map((candidate) => candidate.id));
|
|
@@ -277,7 +270,7 @@ export async function acceptCreativeVariant(options) {
|
|
|
277
270
|
designPatternIds: selected.uxPatternIds,
|
|
278
271
|
capabilityNotes: selected.feasibility.notes,
|
|
279
272
|
},
|
|
280
|
-
nextStep: "Run `vise plan . --request <original request>` or `vise workplan next . --request <original request>`; Vise will include this accepted creative selection as advisory implementation context.",
|
|
273
|
+
nextStep: "Run `vise experience compile .` to compile this accepted selection into an implementation plan (the next stage of the engagement-intelligence pipeline). Then run `vise plan . --request <original request>` or `vise workplan next . --request <original request>`; Vise will include this accepted creative selection as advisory implementation context.",
|
|
281
274
|
};
|
|
282
275
|
if (options.write !== false) {
|
|
283
276
|
selection.artifacts = await writeCreativeSelection(repoRoot, selection);
|
|
@@ -286,27 +279,14 @@ export async function acceptCreativeVariant(options) {
|
|
|
286
279
|
}
|
|
287
280
|
return selection;
|
|
288
281
|
}
|
|
289
|
-
/**
|
|
290
|
-
* Record a no-fit: the driving agent reported that NO catalog variant fits the request (the
|
|
291
|
-
* grounding contract's honest "none" answer). This completes the no-fit path -- instead of the signal
|
|
292
|
-
* being lost ("just tell the user"), it is grounded and persisted as a structured, HUMAN-reviewable
|
|
293
|
-
* catalog-gap record. It does NOT accept a variant, write a selection, build a UX harness, change the
|
|
294
|
-
* catalog, or upload anything (Calibration Boundary). A human reads the record and decides whether the
|
|
295
|
-
* catalog needs a new variant. Separate from acceptCreativeVariant so a no-fit never produces a
|
|
296
|
-
* variant-less CreativeSelection.
|
|
297
|
-
*/
|
|
298
282
|
export async function recordCatalogGap(options) {
|
|
299
283
|
const repoRoot = path.resolve(options.repoPath);
|
|
300
284
|
const briefPath = creativeBriefPath(repoRoot, options.briefPath);
|
|
301
285
|
const brief = await readCreativeBrief(repoRoot, options.briefPath);
|
|
302
|
-
// Ground the no-fit answer. groundSelection requires a rationale (what is missing) even for "none";
|
|
303
|
-
// a no-fit with no reason is useless, so it is rejected here just like a rationale-less accept.
|
|
304
286
|
const grounding = groundSelection({ variantId: NO_FIT, rationale: options.rationale, confidence: options.confidence }, brief.candidateSolutions.map((candidate) => candidate.id));
|
|
305
287
|
if (grounding.status === "rejected") {
|
|
306
288
|
throw new Error(`No-fit record was rejected: ${grounding.signals.filter((signal) => signal.severity === "error").map((signal) => signal.message).join(" ")}`);
|
|
307
289
|
}
|
|
308
|
-
// Optional nearest existing variant, validated against the FULL catalog (a gated variant is a valid
|
|
309
|
-
// "closest" -- e.g. the gap may be served by something still in development).
|
|
310
290
|
let closestVariantId = null;
|
|
311
291
|
let closestWhenToChoose = null;
|
|
312
292
|
const closest = options.closest?.trim();
|
|
@@ -407,12 +387,19 @@ async function loadCatalog() {
|
|
|
407
387
|
}
|
|
408
388
|
async function readCreativeBrief(repoRoot, inputPath) {
|
|
409
389
|
const filePath = creativeBriefPath(repoRoot, inputPath);
|
|
390
|
+
const rel = repoRelativePath(repoRoot, filePath);
|
|
391
|
+
let raw;
|
|
410
392
|
try {
|
|
411
|
-
|
|
393
|
+
raw = await readFile(filePath, "utf8");
|
|
412
394
|
}
|
|
413
|
-
catch
|
|
414
|
-
|
|
415
|
-
|
|
395
|
+
catch {
|
|
396
|
+
throw new Error(`Unable to read creative brief at ${rel}. Run \`vise creative . --request "..."\` first.`);
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
return JSON.parse(raw);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
throw new Error(`Creative brief at ${rel} is present but could not be parsed (corrupt JSON). Re-run \`vise creative . --request "..."\` to regenerate it.`);
|
|
416
403
|
}
|
|
417
404
|
}
|
|
418
405
|
function creativeBriefPath(repoRoot, inputPath) {
|
|
@@ -502,8 +489,6 @@ async function rankVariants(catalog, text, goals, archetypes, inspection, reques
|
|
|
502
489
|
const archetypeScores = new Map(archetypes.map((archetype) => [archetype.id, archetype.score]));
|
|
503
490
|
const patternById = new Map(catalog.solutionPatterns.map((pattern) => [pattern.id, pattern]));
|
|
504
491
|
const uxById = new Map(catalog.uxPatterns.map((pattern) => [pattern.id, pattern]));
|
|
505
|
-
// Availability gate: in-development variants stay in the catalog but are never surfaced to
|
|
506
|
-
// agents/customers (the brief, and therefore accept, can only reach available variants).
|
|
507
492
|
const availableVariants = catalog.variants.filter((variant) => variant.availability !== "in-development");
|
|
508
493
|
const scored = availableVariants.map((variant) => {
|
|
509
494
|
const goalScore = variant.businessGoalIds.reduce((sum, id) => sum + (goalScores.has(id) ? 4 + (goalScores.get(id) ?? 0) : 0), 0);
|
|
@@ -514,8 +499,6 @@ async function rankVariants(catalog, text, goals, archetypes, inspection, reques
|
|
|
514
499
|
const uxScore = variant.uxPatternIds.reduce((sum, id) => sum + scoreKeywords(text, termsForUx(uxById.get(id))), 0);
|
|
515
500
|
return { variant, score: goalScore + archetypeScore + labelScore + patternScore + objectScore + uxScore };
|
|
516
501
|
});
|
|
517
|
-
// Surface the FULL catalog as the agent's choice space (Option A): the keyword score only orders the
|
|
518
|
-
// list (advisory) and flags the top as `recommended` — it must not gate which variants are reachable.
|
|
519
502
|
const top = scored.sort((a, b) => b.score - a.score || a.variant.id.localeCompare(b.variant.id));
|
|
520
503
|
const topOrFallback = top.length > 0 ? top : catalog.variants.map((variant) => ({ variant, score: 0 }));
|
|
521
504
|
const candidates = [];
|
|
@@ -1004,7 +987,6 @@ function assumptionsFor(mode, requirements, prototype, inspection) {
|
|
|
1004
987
|
function reasonForVariant(variant, goals, archetypes, feasibility, isFallbackDefault) {
|
|
1005
988
|
const matchedGoals = variant.businessGoalIds.filter((id) => goals.some((goal) => goal.id === id));
|
|
1006
989
|
const matchedArchetypes = variant.archetypeIds.filter((id) => archetypes.some((archetype) => archetype.id === id));
|
|
1007
|
-
// Non-authoritative framing: the keyword top is the fallback default, not a recommendation.
|
|
1008
990
|
const prefix = isFallbackDefault ? "Strongest keyword match because" : "Included because";
|
|
1009
991
|
const parts = [
|
|
1010
992
|
matchedGoals.length > 0 ? `it supports ${matchedGoals.join(", ")}` : "it provides a distinct engagement direction",
|
|
@@ -1108,8 +1090,6 @@ function shellQuote(value) {
|
|
|
1108
1090
|
function repoRelativePath(repoRoot, filePath) {
|
|
1109
1091
|
return path.relative(repoRoot, filePath).split(path.sep).join("/");
|
|
1110
1092
|
}
|
|
1111
|
-
// Derived from the surface registry + per-object `surface` assignments (src/intelligence/placement.ts).
|
|
1112
|
-
// objectIds come straight from the catalog, so adding an object to a surface is a JSON edit.
|
|
1113
1093
|
const CREATIVE_SURFACE_HINTS = SURFACE_REGISTRY.map((surface) => ({
|
|
1114
1094
|
id: surface.id,
|
|
1115
1095
|
outcome: surface.outcome,
|
|
@@ -1117,12 +1097,6 @@ const CREATIVE_SURFACE_HINTS = SURFACE_REGISTRY.map((surface) => ({
|
|
|
1117
1097
|
objectIds: surfaceObjectIds(surface.id),
|
|
1118
1098
|
reason: "",
|
|
1119
1099
|
}));
|
|
1120
|
-
// GOAL_KEYWORDS / ARCHETYPE_KEYWORDS are the advisory keyword-ranker terms. They are
|
|
1121
|
-
// DERIVED from the catalog `matchTerms` field (business-goals.json / archetypes.json) so a
|
|
1122
|
-
// new goal/archetype is contributable in data alone — no code edit here. Read synchronously
|
|
1123
|
-
// at module load to preserve the synchronous Record<string, string[]> export contract. The
|
|
1124
|
-
// catalog↔map coverage guard (test/run-intelligence-catalog.mjs) and the schema (matchTerms
|
|
1125
|
-
// required, test/run-catalog-schema.mjs) both fail loud if an entry omits matchTerms.
|
|
1126
1100
|
function keywordMapFromCatalog(fileName) {
|
|
1127
1101
|
const filePath = path.join(catalogRoot(), fileName);
|
|
1128
1102
|
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
package/dist/tools/debug.js
CHANGED
|
@@ -2,18 +2,14 @@ import { objectInput, optionalBooleanField, stringField, textResult } from "../t
|
|
|
2
2
|
import { checkCompliance, rulesById } from "./compliance.js";
|
|
3
3
|
import { inspectProject } from "./project.js";
|
|
4
4
|
function sanitizeError(errorMessage) {
|
|
5
|
-
// Strip out local absolute file paths (e.g., /Users/admin/..., /home/user/...)
|
|
6
5
|
let sanitized = errorMessage.replace(/(?:\/(?:users|home)\/[^\s:]+)/gi, "[LOCAL_PATH]");
|
|
7
|
-
// Strip out thread IDs / Hex memory addresses
|
|
8
6
|
sanitized = sanitized.replace(/0x[0-9a-fA-F]+/g, "[HEX_ADDR]");
|
|
9
7
|
return sanitized;
|
|
10
8
|
}
|
|
11
9
|
function extractExceptionClass(errorMessage) {
|
|
12
|
-
// Try to find a Java/Kotlin exception (e.g. java.lang.NullPointerException or com.amity.AmityException)
|
|
13
10
|
const javaMatch = errorMessage.match(/([a-zA-Z0-9_.]+[A-Z][a-zA-Z0-9_]*Exception)/);
|
|
14
11
|
if (javaMatch)
|
|
15
12
|
return javaMatch[1];
|
|
16
|
-
// Try to find a JS Error (e.g. TypeError, Error)
|
|
17
13
|
const jsMatch = errorMessage.match(/([A-Z][a-zA-Z0-9]*Error):/);
|
|
18
14
|
if (jsMatch)
|
|
19
15
|
return jsMatch[1];
|
|
@@ -22,18 +18,11 @@ function extractExceptionClass(errorMessage) {
|
|
|
22
18
|
export async function debugIssue(repoPath, errorMessage, options = {}) {
|
|
23
19
|
const sanitizedError = sanitizeError(errorMessage);
|
|
24
20
|
const exceptionClass = extractExceptionClass(errorMessage);
|
|
25
|
-
// 1. Inspect Context
|
|
26
21
|
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
22
|
let likelyCause = "";
|
|
33
23
|
if (errorMessage.includes("method not found") || errorMessage.includes("Unresolved reference")) {
|
|
34
24
|
likelyCause = "Potential Version Mismatch: The codebase is using a newer API pattern against an older SDK version installed in the project.";
|
|
35
25
|
}
|
|
36
|
-
// 3. Compliance History Correlation
|
|
37
26
|
const correlatedRules = [];
|
|
38
27
|
let checkResult = null;
|
|
39
28
|
try {
|
|
@@ -41,7 +30,6 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
|
|
|
41
30
|
const rulesMap = await rulesById();
|
|
42
31
|
for (const rule of checkResult.rules) {
|
|
43
32
|
if (rule.status === "deterministic-fail" || rule.status === "attestation-needed") {
|
|
44
|
-
// It failed. Check if symptoms match.
|
|
45
33
|
const contractRuleId = rule.contractRuleId ?? rule.ruleId;
|
|
46
34
|
const ruleDef = rulesMap.get(contractRuleId);
|
|
47
35
|
const symptoms = ruleDef?.symptoms || [];
|
|
@@ -54,7 +42,6 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
|
|
|
54
42
|
}
|
|
55
43
|
}
|
|
56
44
|
else if (rule.status === "attested") {
|
|
57
|
-
// Check for false-positive attestations
|
|
58
45
|
const contractRuleId = rule.contractRuleId ?? rule.ruleId;
|
|
59
46
|
const ruleDef = rulesMap.get(contractRuleId);
|
|
60
47
|
const symptoms = ruleDef?.symptoms || [];
|
|
@@ -70,7 +57,6 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
|
|
|
70
57
|
}
|
|
71
58
|
}
|
|
72
59
|
catch (err) {
|
|
73
|
-
// Ignore error if compliance sidecar doesn't exist yet
|
|
74
60
|
}
|
|
75
61
|
if (!likelyCause && correlatedRules.length > 0) {
|
|
76
62
|
likelyCause = `The crash is likely caused by the non-compliant implementation of ${correlatedRules.map(r => r.ruleId).join(", ")}.`;
|
|
@@ -78,7 +64,6 @@ export async function debugIssue(repoPath, errorMessage, options = {}) {
|
|
|
78
64
|
else if (!likelyCause) {
|
|
79
65
|
likelyCause = `An unknown ${exceptionClass || "runtime"} error occurred.`;
|
|
80
66
|
}
|
|
81
|
-
// Build rule-specific remediation guidance from rule definitions and validator findings.
|
|
82
67
|
const suggestedRemediation = buildRemediation(correlatedRules, checkResult);
|
|
83
68
|
const repairBrief = buildRepairBrief(correlatedRules, checkResult, inspection.platforms, likelyCause);
|
|
84
69
|
const payload = {
|
|
@@ -111,7 +96,6 @@ function buildRemediation(correlatedRules, checkResult) {
|
|
|
111
96
|
}
|
|
112
97
|
const rules = correlatedRules.map(cr => {
|
|
113
98
|
const ruleResult = checkResult?.rules.find(r => r.ruleId === cr.ruleId);
|
|
114
|
-
// Prefer the validator's per-finding recommendation; fall back to a generic note.
|
|
115
99
|
const guidance = ruleResult?.recommendation
|
|
116
100
|
?? "Review the rule implementation and ensure the SDK integration matches the required pattern.";
|
|
117
101
|
return {
|