@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +71 -24
  2. package/LICENSE +8 -6
  3. package/README.md +168 -358
  4. package/dist/capabilities.js +19 -62
  5. package/dist/intelligence/grounding.js +0 -23
  6. package/dist/intelligence/placement.js +0 -9
  7. package/dist/outcomes.js +44 -22
  8. package/dist/server.js +75 -38
  9. package/dist/tools/ast.js +3 -209
  10. package/dist/tools/blocks.js +6 -20
  11. package/dist/tools/compliance.js +168 -43
  12. package/dist/tools/creative.js +15 -41
  13. package/dist/tools/debug.js +0 -16
  14. package/dist/tools/design.js +18 -364
  15. package/dist/tools/docs.js +53 -24
  16. package/dist/tools/experienceCompiler.js +7 -10
  17. package/dist/tools/experienceSensors.js +1 -1
  18. package/dist/tools/harness.js +2 -27
  19. package/dist/tools/integration.js +6 -38
  20. package/dist/tools/learning.js +1 -1
  21. package/dist/tools/project.js +763 -546
  22. package/dist/tools/sdkFacts.js +2 -15
  23. package/dist/tools/sdkVersion.js +3 -36
  24. package/dist/tools/sensors.js +0 -6
  25. package/dist/tools/uxHarness.js +12 -9
  26. package/package.json +8 -97
  27. package/rules/chat.yaml +225 -0
  28. package/rules/event.yaml +45 -0
  29. package/rules/feed.yaml +24 -24
  30. package/rules/invitation.yaml +58 -0
  31. package/rules/live-data.yaml +104 -2
  32. package/rules/notification-tray.yaml +106 -0
  33. package/rules/poll.yaml +71 -0
  34. package/rules/sdk-lifecycle.yaml +112 -6
  35. package/rules/search.yaml +131 -0
  36. package/rules/story.yaml +221 -0
  37. package/rules/user-blocking.yaml +71 -0
  38. package/sdk-surface/flutter.json +1 -1
  39. package/sdk-surface/ios.json +1 -1
  40. package/sdk-surface/manifest.json +12 -12
  41. package/sdk-surface/models.flutter.json +96 -96
  42. package/sdk-surface/models.ios.json +1 -1
  43. package/sdk-surface/typescript.json +4 -4
  44. package/skills/social-plus-vise/SKILL.md +25 -5
  45. package/scripts/catalog-coverage-html.mjs +0 -325
  46. package/scripts/catalog-relationships-html.mjs +0 -686
  47. package/scripts/catalog-sheets.mjs +0 -286
  48. package/scripts/dart-model-extractor/bin/extract_models.dart +0 -169
  49. package/scripts/dart-model-extractor/pubspec.lock +0 -149
  50. package/scripts/dart-model-extractor/pubspec.yaml +0 -16
  51. package/scripts/extract-sdk-models.mjs +0 -749
  52. package/scripts/import-sdk-surface.mjs +0 -161
  53. package/scripts/pilot-feedback.mjs +0 -107
  54. package/scripts/workshop-board-html.mjs +0 -1018
  55. package/scripts/workshop-kit.mjs +0 -252
  56. package/skills/vise-harness-engineer/SKILL.md +0 -35
@@ -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; v0.5 will route review-queue uploads using these fields.",
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 local-only in v0.4.x. v0.5 review queue will route uploads using these fields.",
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); // minimal shape — stable digest input
321
- const fileRefs = rules.map(ruleRefForFile); // adds title for human/agent readers
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. Re-run `vise init` with --answer for each blocking question, or pass --allow-unresolved-intake only for retrospective/harness initialization.",
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), // hash of minimal refs (no title)
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, // file carries titles
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
- ...(attestable && {
617
- next_step: `Provide the file(s) listed in blockers_fired, then run \`vise attest . --rule ${rule.id}\` to record your review decision.`,
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 dogfood and benchmark evidence calibrates it responsibly.",
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
- // Benchmark-measured friction: agents looped on attest dialect for ~25 min/cell when docs and SDK
1161
- // disagreed on exact invocation syntax (capability-matrix 2026-06, Row 5). Hand them the exact incantation.
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
- return JSON.parse(await readFile(file, "utf8"));
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);
@@ -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 shadow-policy-v2-draft candidate ranking. Defaults to false and does not change the default candidate order.",
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. The factory will validate (ground) your choice against the catalog; it does not infer the variant for you.",
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`. The factory 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>\"`.",
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
- return JSON.parse(await readFile(filePath, "utf8"));
393
+ raw = await readFile(filePath, "utf8");
412
394
  }
413
- catch (error) {
414
- const detail = error instanceof Error ? error.message : String(error);
415
- throw new Error(`Unable to read creative brief at ${repoRelativePath(repoRoot, filePath)}. Run \`vise creative . --request "..."\` first. ${detail}`);
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"));
@@ -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 {