@amityco/social-plus-vise 0.14.26 → 0.14.28

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 (43) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +25 -7
  3. package/dist/capabilities.js +45 -1
  4. package/dist/intelligence/grounding.js +107 -0
  5. package/dist/intelligence/placement.js +71 -0
  6. package/dist/outcomes.js +2 -2
  7. package/dist/server.js +189 -10
  8. package/dist/tools/ast.js +161 -9
  9. package/dist/tools/blocks.js +95 -2
  10. package/dist/tools/compliance.js +176 -15
  11. package/dist/tools/creative.js +542 -106
  12. package/dist/tools/experienceCompiler.js +439 -0
  13. package/dist/tools/experienceSensors.js +406 -0
  14. package/dist/tools/harness.js +141 -8
  15. package/dist/tools/integration.js +43 -2
  16. package/dist/tools/learning.js +486 -0
  17. package/dist/tools/project.js +147 -17
  18. package/dist/tools/sdkFacts.js +83 -6
  19. package/dist/tools/sensors.js +38 -0
  20. package/dist/tools/uxHarness.js +546 -0
  21. package/package.json +45 -9
  22. package/packages/intelligence/README.md +1 -1
  23. package/packages/intelligence/catalog/archetypes.json +10 -0
  24. package/packages/intelligence/catalog/business-goals.json +8 -0
  25. package/packages/intelligence/catalog/catalog.schema.json +224 -0
  26. package/packages/intelligence/catalog/experience-objects.json +18 -18
  27. package/packages/intelligence/catalog/variants.json +56 -7
  28. package/scripts/catalog-coverage-html.mjs +325 -0
  29. package/scripts/catalog-relationships-html.mjs +686 -0
  30. package/scripts/catalog-sheets.mjs +286 -0
  31. package/scripts/extract-sdk-models.mjs +408 -0
  32. package/scripts/import-sdk-surface.mjs +43 -3
  33. package/scripts/pilot-feedback.mjs +107 -0
  34. package/scripts/workshop-board-html.mjs +1018 -0
  35. package/scripts/workshop-kit.mjs +252 -0
  36. package/sdk-surface/manifest.json +48 -2
  37. package/sdk-surface/models.android.json +990 -0
  38. package/sdk-surface/models.flutter.json +980 -0
  39. package/sdk-surface/models.ios.json +980 -0
  40. package/sdk-surface/models.typescript.json +1304 -0
  41. package/skills/social-plus-vise/SKILL.md +1 -1
  42. package/skills/vise-harness-engineer/SKILL.md +1 -1
  43. package/social.plus-vise.png +0 -0
package/dist/server.js CHANGED
@@ -6,11 +6,14 @@ import { fileURLToPath } from "node:url";
6
6
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
- import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
9
+ import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
10
10
  import { designCheckTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
11
11
  import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
12
+ import { compileExperienceTool } from "./tools/experienceCompiler.js";
13
+ import { experienceSensorsTool } from "./tools/experienceSensors.js";
12
14
  import { planHarnessTool } from "./tools/harness.js";
13
15
  import { planIntegrationTool } from "./tools/integration.js";
16
+ import { recordLearningTool, showLearningTool } from "./tools/learning.js";
14
17
  import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
15
18
  import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
16
19
  import { runSensorsTool } from "./tools/sensors.js";
@@ -18,6 +21,7 @@ import { getSdkFactsTool } from "./tools/sdkFacts.js";
18
21
  import { addBlockInstall, listRegistryBlocks, planBlockInstall, validateBlockInstall } from "./tools/blocks.js";
19
22
  import { debugIssueTool, debugIssue } from "./tools/debug.js";
20
23
  import { creativeAcceptTool, creativeBriefTool } from "./tools/creative.js";
24
+ import { uxHarnessTool } from "./tools/uxHarness.js";
21
25
  import { packageName, packageVersion } from "./version.js";
22
26
  const tools = new Map([
23
27
  searchDocsTool,
@@ -25,10 +29,16 @@ const tools = new Map([
25
29
  inspectProjectTool,
26
30
  creativeBriefTool,
27
31
  creativeAcceptTool,
32
+ uxHarnessTool,
33
+ compileExperienceTool,
34
+ experienceSensorsTool,
28
35
  planHarnessTool,
29
36
  planIntegrationTool,
30
37
  initComplianceTool,
31
38
  checkComplianceTool,
39
+ experienceReportTool,
40
+ recordLearningTool,
41
+ showLearningTool,
32
42
  syncComplianceTool,
33
43
  attestRuleTool,
34
44
  explainRuleTool,
@@ -140,15 +150,18 @@ async function handleCli(args) {
140
150
  if (command === "creative") {
141
151
  if (args[1] === "accept") {
142
152
  const subArgs = args.slice(2);
143
- assertOnlyKnownFlags(subArgs, ["variant", "brief", "brief-path"], "creative accept");
153
+ assertOnlyKnownFlags(subArgs, ["variant", "brief", "brief-path", "rationale", "confidence", "closest"], "creative accept");
144
154
  await printToolResult(creativeAcceptTool, {
145
155
  repoPath: positionalRepoPath(subArgs),
146
- variantId: requiredFlagValue(subArgs, "variant", "creative accept requires --variant <id>."),
156
+ variantId: requiredFlagValue(subArgs, "variant", "creative accept requires --variant <id> (or --variant none to record a catalog gap)."),
147
157
  briefPath: flagValue(subArgs, "brief") ?? flagValue(subArgs, "brief-path"),
158
+ rationale: flagValue(subArgs, "rationale"),
159
+ confidence: flagValue(subArgs, "confidence"),
160
+ closest: flagValue(subArgs, "closest"),
148
161
  });
149
162
  return "exit";
150
163
  }
151
- assertOnlyKnownFlags(args, ["request", "requirements", "prototype", "surface", "surface-path", "no-requirements", "no-write"], "creative");
164
+ assertOnlyKnownFlags(args, ["request", "requirements", "prototype", "surface", "surface-path", "no-requirements", "no-write", "ranking-preview"], "creative");
152
165
  if (hasFlag(args, "no-requirements") && flagValue(args, "requirements")) {
153
166
  throw new Error("creative accepts either --requirements <path> or --no-requirements, not both.");
154
167
  }
@@ -158,10 +171,82 @@ async function handleCli(args) {
158
171
  surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
159
172
  requirementsPath: hasFlag(args, "no-requirements") ? "none" : flagValue(args, "requirements"),
160
173
  prototypePath: flagValue(args, "prototype"),
174
+ rankingPreview: hasFlag(args, "ranking-preview"),
161
175
  write: !hasFlag(args, "no-write"),
162
176
  });
163
177
  return "exit";
164
178
  }
179
+ if (command === "ux-harness") {
180
+ assertOnlyKnownFlags(args, ["surface", "surface-path", "no-write"], "ux-harness");
181
+ await printToolResult(uxHarnessTool, {
182
+ repoPath: positionalRepoPath(args.slice(1)),
183
+ surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
184
+ write: !hasFlag(args, "no-write"),
185
+ });
186
+ return "exit";
187
+ }
188
+ if (command === "experience-report" || command === "experience_report") {
189
+ assertOnlyKnownFlags(args, ["no-write"], "experience-report");
190
+ await printToolResult(experienceReportTool, {
191
+ repoPath: positionalRepoPath(args.slice(1)),
192
+ write: !hasFlag(args, "no-write"),
193
+ });
194
+ return "exit";
195
+ }
196
+ if (command === "learning" || command === "learn") {
197
+ const sub = args[1];
198
+ const subArgs = args.slice(2);
199
+ if (sub === "record") {
200
+ assertOnlyKnownFlags(subArgs, ["kind", "sentiment", "variant", "variant-id", "note", "metric", "no-write"], "learning record");
201
+ await printToolResult(recordLearningTool, {
202
+ repoPath: positionalRepoPath(subArgs),
203
+ kind: flagValue(subArgs, "kind"),
204
+ sentiment: flagValue(subArgs, "sentiment"),
205
+ variantId: flagValue(subArgs, "variant") ?? flagValue(subArgs, "variant-id"),
206
+ note: flagValue(subArgs, "note"),
207
+ metrics: keyValueFlag(subArgs, "metric"),
208
+ write: !hasFlag(subArgs, "no-write"),
209
+ });
210
+ return "exit";
211
+ }
212
+ if (sub === "show" || sub === "summary") {
213
+ assertOnlyKnownFlags(subArgs, [], `learning ${sub}`);
214
+ await printToolResult(showLearningTool, {
215
+ repoPath: positionalRepoPath(subArgs),
216
+ });
217
+ return "exit";
218
+ }
219
+ console.error(`Unknown learning subcommand: ${sub ?? "(none)"}. Expected "record" or "show".`);
220
+ process.exitCode = 1;
221
+ return "exit";
222
+ }
223
+ if (command === "experience") {
224
+ const sub = args[1];
225
+ const subArgs = args.slice(2);
226
+ if (sub === "compile") {
227
+ assertOnlyKnownFlags(subArgs, ["request", "surface", "surface-path", "registry", "no-write"], "experience compile");
228
+ await printToolResult(compileExperienceTool, {
229
+ repoPath: positionalRepoPath(subArgs),
230
+ request: flagValue(subArgs, "request"),
231
+ surfacePath: flagValue(subArgs, "surface") ?? flagValue(subArgs, "surface-path"),
232
+ registryPath: flagValue(subArgs, "registry"),
233
+ write: !hasFlag(subArgs, "no-write"),
234
+ });
235
+ return "exit";
236
+ }
237
+ if (sub === "sensors") {
238
+ assertOnlyKnownFlags(subArgs, ["surface", "surface-path", "no-write"], "experience sensors");
239
+ await printToolResult(experienceSensorsTool, {
240
+ repoPath: positionalRepoPath(subArgs),
241
+ surfacePath: flagValue(subArgs, "surface") ?? flagValue(subArgs, "surface-path"),
242
+ write: !hasFlag(subArgs, "no-write"),
243
+ });
244
+ return "exit";
245
+ }
246
+ console.error(`Unknown experience subcommand: ${sub ?? "(none)"}. Expected "compile" or "sensors".`);
247
+ process.exitCode = 1;
248
+ return "exit";
249
+ }
165
250
  if (command === "debug") {
166
251
  assertOnlyKnownFlags(args, ["error", "error-file", "brief"], "debug");
167
252
  let errorMessage = flagValue(args, "error");
@@ -231,7 +316,7 @@ async function handleCli(args) {
231
316
  return "exit";
232
317
  }
233
318
  if (command === "run-sensors" || command === "run_sensor" || command === "run-sensor") {
234
- await printToolResult(runSensorsTool, {
319
+ const printed = await printToolResult(runSensorsTool, {
235
320
  repoPath: positionalRepoPath(args.slice(1)),
236
321
  request: flagValue(args, "request"),
237
322
  include: flagValues(args, "include"),
@@ -239,6 +324,13 @@ async function handleCli(args) {
239
324
  dryRun: hasFlag(args, "dry-run"),
240
325
  surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
241
326
  });
327
+ // Reflect sensor outcome in the exit code so `vise run-sensors` is usable as a CI gate.
328
+ // Previously it always exited 0, so a failed/timed-out build silently read as green.
329
+ // dry-run / passed / no-sensors stay 0; only real failures fail.
330
+ const status = toolResultStatus(printed);
331
+ if (status === "failed" || status === "timed-out") {
332
+ process.exitCode = 1;
333
+ }
242
334
  return "exit";
243
335
  }
244
336
  if (command === "resolve") {
@@ -250,7 +342,7 @@ async function handleCli(args) {
250
342
  return "exit";
251
343
  }
252
344
  if (command === "sdk-facts" || command === "sdk_facts") {
253
- assertOnlyKnownFlags(args, ["platform", "capability", "surface-dir", "format", "include-symbols"], "sdk-facts");
345
+ assertOnlyKnownFlags(args, ["platform", "capability", "surface-dir", "format", "include-symbols", "include-models"], "sdk-facts");
254
346
  const format = flagValue(args, "format") ?? "json";
255
347
  if (format !== "json") {
256
348
  throw new Error("sdk-facts currently supports --format json only.");
@@ -260,6 +352,7 @@ async function handleCli(args) {
260
352
  capability: flagValue(args, "capability"),
261
353
  surfaceDir: flagValue(args, "surface-dir"),
262
354
  includeSymbols: hasFlag(args, "include-symbols"),
355
+ includeModels: hasFlag(args, "include-models"),
263
356
  });
264
357
  return "exit";
265
358
  }
@@ -490,13 +583,74 @@ Usage:
490
583
  vise creative [repoPath] --request "Add engagement" --requirements none
491
584
  vise creative [repoPath] --request "Add engagement" --no-requirements
492
585
  vise creative [repoPath] --request "Add engagement" --prototype ./prototype.html
586
+ vise creative [repoPath] --request "Add engagement" --ranking-preview
493
587
  vise creative [repoPath] --request "Add engagement" --no-write
494
588
  vise creative accept [repoPath] --variant community-first
589
+ vise creative accept [repoPath] --variant none --rationale "No variant fits X; closest is Y" --closest discovery-first
495
590
 
496
591
  Output:
497
592
  Writes sp-vise/creative-brief.json and sp-vise/creative-brief.md unless --no-write is set.
593
+ --ranking-preview also writes sp-vise/candidate-ranking-preview.json as an opt-in advisory local preview.
498
594
  Requirements are optional; absence or explicit none switches creative mode to exploratory.
499
- creative accept writes sp-vise/creative-selection.json for plan/init/workplan feed-forward.`;
595
+ creative accept writes sp-vise/creative-selection.json for plan/init/workplan feed-forward.
596
+ creative accept --variant none records sp-vise/catalog-gap.json (a local no-fit signal for human catalog review).`;
597
+ }
598
+ if (command === "ux-harness") {
599
+ return `${packageName} ux-harness
600
+
601
+ Generate the advisory UX Harness sidecar from an accepted creative selection.
602
+
603
+ Usage:
604
+ vise ux-harness [repoPath]
605
+ vise ux-harness [repoPath] --surface apps/web
606
+ vise ux-harness [repoPath] --no-write
607
+
608
+ Output:
609
+ Writes sp-vise/ux-harness.json by default. The sidecar contains selected UX pattern expectations, tradeoffs, anti-patterns, and advisory conformance hints. It never changes deterministic compliance rules by itself.`;
610
+ }
611
+ if (command === "experience-report" || command === "experience_report") {
612
+ return `${packageName} experience-report
613
+
614
+ Generate the advisory dimensioned Experience Report from current compliance evidence.
615
+
616
+ Usage:
617
+ vise experience-report [repoPath]
618
+ vise experience-report [repoPath] --no-write
619
+
620
+ Output:
621
+ Writes sp-vise/experience-report.json by default. The report summarizes technical compliance, design contract presence, UX Harness advisory evidence, and business alignment without producing a calibrated single score.`;
622
+ }
623
+ if (command === "learning" || command === "learn") {
624
+ return `${packageName} learning
625
+
626
+ Record and review local-only Engagement Intelligence learning events.
627
+
628
+ Usage:
629
+ vise learning record [repoPath] --sentiment positive --note "Customer preferred Community First"
630
+ vise learning record [repoPath] --kind outcome-snapshot --metric retention_delta=0.12
631
+ vise learning record [repoPath] --kind report-review --sentiment neutral --no-write
632
+ vise learning show [repoPath]
633
+
634
+ Output:
635
+ record writes sp-vise/learning-events.jsonl and sp-vise/learning-summary.json by default. Learning events are local evidence only; Vise does not change recommendation ranking from them yet.`;
636
+ }
637
+ if (command === "experience") {
638
+ return `${packageName} experience
639
+
640
+ Compile an accepted Engagement Intelligence variant and summarize experience sensors.
641
+
642
+ Usage:
643
+ vise experience compile [repoPath]
644
+ vise experience compile [repoPath] --request "Make this app social"
645
+ vise experience compile [repoPath] --registry ./registry/blocks.json
646
+ vise experience compile [repoPath] --surface apps/web --no-write
647
+ vise experience sensors [repoPath]
648
+ vise experience sensors [repoPath] --surface apps/web --no-write
649
+
650
+ Output:
651
+ compile writes sp-vise/experience-compiler.json by default. The artifact summarizes install guidance, navigation wiring, per-surface plan/init commands, UX expectations, optional Block Factory bridge candidates, design adaptation, and validation commands. It does not edit source code.
652
+
653
+ sensors writes sp-vise/experience-sensors.json by default. The artifact summarizes advisory technical, design, UX, accessibility, and business-alignment evidence without changing vise check exit codes or producing a calibrated score.`;
500
654
  }
501
655
  if (command === "plan-harness") {
502
656
  return `${packageName} plan-harness
@@ -623,10 +777,15 @@ Usage:
623
777
  return `${packageName} sdk-facts
624
778
 
625
779
  Read bundled SDK surface facts for social.plus Block Factory planning. Internal, projectless, and read-only.
780
+ Symbol facts prove existence; --include-models adds extraction-grounded field-level model schemas
781
+ (modelSchemas, plus capability model entries gain a schema). Platforms without grounded field data
782
+ report modelFacts.status=absent instead of fabricated fields.
626
783
 
627
784
  Usage:
628
785
  vise sdk-facts --platform typescript --capability comments --format json
786
+ vise sdk-facts --platform typescript --capability comments --include-models
629
787
  vise sdk-facts --platform react-native --capability reactions --include-symbols
788
+ vise sdk-facts --platform android --include-models --format json
630
789
  vise sdk-facts --platform android --surface-dir ./sdk-surface --format json`;
631
790
  }
632
791
  if (command === "blocks") {
@@ -745,7 +904,13 @@ Usage:
745
904
  vise print-skill Print bundled skill markdown
746
905
  vise inspect [repoPath] Inspect platform and design signals
747
906
  vise creative [repoPath] --request "..." Create an Engagement Intelligence brief
748
- vise creative accept [repoPath] --variant <id> Accept a creative variant
907
+ vise creative accept [repoPath] --variant <id|none> Accept a creative variant, or record a no-fit catalog gap with --variant none
908
+ vise ux-harness [repoPath] Generate UX Harness expectations from creative selection
909
+ vise experience-report [repoPath] Write the advisory dimensioned Experience Report
910
+ vise experience compile [repoPath] Compile selected variant into implementation artifacts
911
+ vise experience sensors [repoPath] Write advisory experience sensor framework
912
+ vise learning record [repoPath] Record a local-only learning event
913
+ vise learning show [repoPath] Show local learning summary
749
914
  vise debug [repoPath] --error ... Debug an SDK-specific runtime error and emit a repair brief
750
915
  vise plan [repoPath] --request "..." Create an implementation plan
751
916
  vise workplan next [repoPath] --request "..." Get the next broad-social surface to implement
@@ -784,7 +949,19 @@ SOCIAL_PLUS_DOCS_ROOT to test against a local social-plus-docs checkout.`;
784
949
  }
785
950
  async function printToolResult(tool, input) {
786
951
  const result = await tool.call(input);
787
- console.log(result.content.map((item) => item.text).join("\n"));
952
+ const text = result.content.map((item) => item.text).join("\n");
953
+ console.log(text);
954
+ return { result, text };
955
+ }
956
+ // Parse the `status` field from a tool's printed JSON payload (best-effort).
957
+ function toolResultStatus(printed) {
958
+ try {
959
+ const payload = JSON.parse(printed.text);
960
+ return typeof payload.status === "string" ? payload.status : undefined;
961
+ }
962
+ catch {
963
+ return undefined;
964
+ }
788
965
  }
789
966
  async function installSkill(args) {
790
967
  const source = skillSourceDir();
@@ -1021,6 +1198,7 @@ async function workplanStatus(args) {
1021
1198
  request,
1022
1199
  progressPath: workplanProgressPath(repoRoot),
1023
1200
  creativeContext: plan.socialWorkplan?.creativeContext ?? plan.creativeContext,
1201
+ uxHarness: plan.socialWorkplan?.uxHarness ?? plan.uxHarness,
1024
1202
  completed,
1025
1203
  sequence: sequence.map((surface) => ({
1026
1204
  id: surface.id,
@@ -1037,6 +1215,7 @@ async function workplanStatus(args) {
1037
1215
  label: nextSurface.label,
1038
1216
  intake: nextSurface.intake,
1039
1217
  validation: nextSurface.validation,
1218
+ uxHarness: nextSurface.uxHarness,
1040
1219
  commands: {
1041
1220
  plan: nextSurface.planCommand,
1042
1221
  init: nextSurface.initCommand,
@@ -1222,7 +1401,7 @@ function ciCheckResult(result) {
1222
1401
  };
1223
1402
  }
1224
1403
  function positionalRepoPath(args) {
1225
- const flagsWithValues = new Set(["request", "requirements", "prototype", "variant", "brief", "brief-path", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source", "note"]);
1404
+ const flagsWithValues = new Set(["request", "requirements", "prototype", "variant", "variant-id", "brief", "brief-path", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source", "note", "kind", "sentiment", "metric"]);
1226
1405
  for (let index = 0; index < args.length; index += 1) {
1227
1406
  const arg = args[index];
1228
1407
  if (!arg) {
package/dist/tools/ast.js CHANGED
@@ -29,11 +29,21 @@ function loadNativeBindings() {
29
29
  const ParserCtor = nodeRequire("tree-sitter");
30
30
  const tsGrammars = nodeRequire("tree-sitter-typescript");
31
31
  const kotlinGrammar = nodeRequire("tree-sitter-kotlin");
32
+ // Swift loads in its own try/catch: a missing tree-sitter-swift prebuild on an
33
+ // exotic platform must degrade ONLY the Swift helpers, not all AST analysis.
34
+ let swiftGrammar = null;
35
+ try {
36
+ swiftGrammar = nodeRequire("tree-sitter-swift");
37
+ }
38
+ catch {
39
+ swiftGrammar = null;
40
+ }
32
41
  nativeBindings = {
33
42
  Parser: ParserCtor,
34
43
  tsGrammar: tsGrammars.typescript,
35
44
  tsxGrammar: tsGrammars.tsx,
36
45
  kotlinGrammar,
46
+ swiftGrammar,
37
47
  };
38
48
  }
39
49
  catch {
@@ -50,6 +60,14 @@ function loadNativeBindings() {
50
60
  export function astAvailable() {
51
61
  return loadNativeBindings() !== null;
52
62
  }
63
+ /**
64
+ * Whether the Swift grammar specifically is available. tree-sitter-swift is loaded
65
+ * independently of the core bindings (see loadNativeBindings), so Swift-only
66
+ * validators can check this and fall back to regex without disabling ts/tsx/kotlin.
67
+ */
68
+ export function swiftAstAvailable() {
69
+ return loadNativeBindings()?.swiftGrammar != null;
70
+ }
53
71
  /**
54
72
  * Strip comments from source code using tree-sitter AST.
55
73
  * Replaces comment spans with whitespace (preserving line structure).
@@ -101,6 +119,12 @@ function getParser(language) {
101
119
  parser.setLanguage(native.tsxGrammar);
102
120
  else if (language === "kotlin")
103
121
  parser.setLanguage(native.kotlinGrammar);
122
+ else if (language === "swift") {
123
+ if (native.swiftGrammar == null) {
124
+ throw new Error("tree-sitter-swift unavailable; Swift AST analysis disabled (regex fallback in effect)");
125
+ }
126
+ parser.setLanguage(native.swiftGrammar);
127
+ }
104
128
  else
105
129
  parser.setLanguage(native.tsGrammar);
106
130
  parsers.set(language, parser);
@@ -166,7 +190,10 @@ export function findCallExpressions(tree, calleePattern) {
166
190
  results.push({ callee, node, args });
167
191
  return;
168
192
  }
169
- // Kotlin: call_expression = navigation_expression + call_suffix
193
+ // Kotlin AND Swift: call_expression = navigation_expression + call_suffix.
194
+ // (tree-sitter-swift descends from the Kotlin grammar, so the node names —
195
+ // navigation_expression / navigation_suffix / call_suffix / value_arguments —
196
+ // are identical; only Swift's labeled arguments add a value_argument_label.)
170
197
  const navNode = node.namedChild(0);
171
198
  const suffixNode = node.namedChild(1);
172
199
  if (!navNode || !suffixNode || suffixNode.type !== "call_suffix")
@@ -181,8 +208,11 @@ export function findCallExpressions(tree, calleePattern) {
181
208
  for (let i = 0; i < valArgsNode.namedChildCount; i++) {
182
209
  const valArg = valArgsNode.namedChild(i);
183
210
  if (valArg && valArg.type === "value_argument") {
184
- // The actual expression is inside value_argument
185
- const expr = valArg.namedChild(0);
211
+ // The actual expression is inside value_argument. Swift labeled arguments
212
+ // put a value_argument_label first — the value is the last named child.
213
+ // Kotlin behaviour (namedChild(0)) is intentionally unchanged.
214
+ const first = valArg.namedChild(0);
215
+ const expr = first?.type === "value_argument_label" ? valArg.namedChild(valArg.namedChildCount - 1) : first;
186
216
  if (expr)
187
217
  args.push(expr);
188
218
  }
@@ -233,6 +263,93 @@ export function pickObjectProperty(objectNode, propertyName) {
233
263
  }
234
264
  return undefined;
235
265
  }
266
+ /**
267
+ * Pick the value of a labeled argument from a Swift call expression node.
268
+ * E.g., from `client.login(userId: HARDCODED, sessionHandler: handler)`,
269
+ * pick the value node for label "userId".
270
+ *
271
+ * Swift-shaped trees only (call_suffix → value_arguments → value_argument with a
272
+ * value_argument_label). Returns undefined when the label is absent.
273
+ */
274
+ export function pickLabeledArgument(callNode, label) {
275
+ for (let i = 0; i < callNode.namedChildCount; i++) {
276
+ const suffix = callNode.namedChild(i);
277
+ if (!suffix || suffix.type !== "call_suffix")
278
+ continue;
279
+ const valArgs = suffix.namedChild(0);
280
+ if (!valArgs || valArgs.type !== "value_arguments")
281
+ continue;
282
+ for (let j = 0; j < valArgs.namedChildCount; j++) {
283
+ const valArg = valArgs.namedChild(j);
284
+ if (!valArg || valArg.type !== "value_argument")
285
+ continue;
286
+ const first = valArg.namedChild(0);
287
+ if (first?.type !== "value_argument_label" || first.text !== label)
288
+ continue;
289
+ const value = valArg.namedChild(valArg.namedChildCount - 1);
290
+ // namedChild(last) === the label itself when the argument has no value
291
+ // (selector-reference form) — treat as absent.
292
+ return value && value !== first ? value : undefined;
293
+ }
294
+ }
295
+ return undefined;
296
+ }
297
+ /**
298
+ * Find Swift `let`/`var` declarations whose initializer text matches a pattern AND
299
+ * which are function-local (declared inside a function/initializer/closure/computed-
300
+ * property body rather than at type or file scope). Used for retention rules: a
301
+ * function-local binding dies with the stack frame, while a type-scope property
302
+ * lives with the object — a distinction regex bridges cannot make reliably.
303
+ *
304
+ * Conservative direction: a declaration only counts as local when a KNOWN
305
+ * function-ish ancestor encloses it, so unknown containers degrade toward
306
+ * "retained" (quiet), never toward a false positive.
307
+ */
308
+ export function findSwiftFunctionLocalDeclarations(tree, initializerPattern) {
309
+ const results = [];
310
+ walkTree(tree.rootNode, (node) => {
311
+ if (node.type !== "property_declaration")
312
+ return;
313
+ const value = swiftPropertyInitializer(node);
314
+ if (!value || !initializerPattern.test(value.text))
315
+ return;
316
+ if (hasFunctionAncestor(node))
317
+ results.push(node);
318
+ });
319
+ return results;
320
+ }
321
+ const SWIFT_FUNCTION_SCOPES = new Set([
322
+ "function_declaration",
323
+ "init_declaration",
324
+ "deinit_declaration",
325
+ "lambda_literal",
326
+ "computed_property",
327
+ "computed_getter",
328
+ "computed_setter",
329
+ ]);
330
+ function hasFunctionAncestor(node) {
331
+ let current = node.parent;
332
+ while (current) {
333
+ if (SWIFT_FUNCTION_SCOPES.has(current.type))
334
+ return true;
335
+ current = current.parent;
336
+ }
337
+ return false;
338
+ }
339
+ /** The initializer expression of a Swift property_declaration (the value after `=`), if any. */
340
+ function swiftPropertyInitializer(node) {
341
+ // Shape: property_declaration = value_binding_pattern, pattern, [type_annotation], [value expr]
342
+ // The initializer (when present) is the last named child and is none of the structural parts.
343
+ const last = node.namedChild(node.namedChildCount - 1);
344
+ if (!last)
345
+ return undefined;
346
+ if (["value_binding_pattern", "pattern", "type_annotation", "modifiers", "attribute"].includes(last.type))
347
+ return undefined;
348
+ // computed_property is a body, not an initializer.
349
+ if (last.type === "computed_property")
350
+ return undefined;
351
+ return last;
352
+ }
236
353
  // ── Internal helpers ──────────────────────────────────────────────────────────
237
354
  function walkTree(node, visit) {
238
355
  visit(node);
@@ -322,6 +439,25 @@ function extractStringLiteral(node) {
322
439
  return text.slice(1, -1);
323
440
  }
324
441
  }
442
+ // Swift: line_string_literal contains line_str_text chunks; an interpolated
443
+ // string has >1 named child (line_str_text + interpolated_expression) and is
444
+ // NOT statically resolvable — mirror the TS template-literal treatment.
445
+ if (node.type === "line_string_literal") {
446
+ if (node.namedChildCount === 0)
447
+ return ""; // empty string ""
448
+ if (node.namedChildCount === 1 && node.namedChild(0)?.type === "line_str_text") {
449
+ return node.namedChild(0).text;
450
+ }
451
+ return undefined;
452
+ }
453
+ // Swift: multi-line """…""" literal. Swift semantics strip the newline after the
454
+ // opening and before the closing delimiter.
455
+ if (node.type === "multi_line_string_literal") {
456
+ if (node.namedChildCount === 1 && node.namedChild(0)?.type === "multi_line_str_text") {
457
+ return node.namedChild(0).text.replace(/^\n/, "").replace(/\n[ \t]*$/, "");
458
+ }
459
+ return undefined;
460
+ }
325
461
  return undefined;
326
462
  }
327
463
  function resolveIdentifierToLiteral(name, root) {
@@ -345,15 +481,31 @@ function resolveIdentifierToLiteral(name, root) {
345
481
  // Kotlin: property_declaration with variable_declaration + string_literal
346
482
  if (node.type === "property_declaration") {
347
483
  const varDecl = node.namedChildren.find((c) => c.type === "variable_declaration");
348
- if (!varDecl)
484
+ if (varDecl) {
485
+ const idNode = varDecl.namedChildren.find((c) => c.type === "simple_identifier");
486
+ if (!idNode || idNode.text !== name)
487
+ return;
488
+ const strLit = node.namedChildren.find((c) => c.type === "string_literal");
489
+ if (!strLit)
490
+ return;
491
+ const literal = extractStringLiteral(strLit);
492
+ if (literal !== undefined)
493
+ result = literal;
494
+ return;
495
+ }
496
+ // Swift: property_declaration with pattern → simple_identifier; the string
497
+ // literal must be a DIRECT child (the initializer) — a literal nested inside
498
+ // e.g. `env["KEY"] ?? ""` is not a static binding and must not resolve.
499
+ const pattern = node.namedChildren.find((c) => c.type === "pattern");
500
+ if (!pattern)
349
501
  return;
350
- const idNode = varDecl.namedChildren.find((c) => c.type === "simple_identifier");
351
- if (!idNode || idNode.text !== name)
502
+ const swiftId = pattern.namedChildren.find((c) => c.type === "simple_identifier");
503
+ if (!swiftId || swiftId.text !== name)
352
504
  return;
353
- const strLit = node.namedChildren.find((c) => c.type === "string_literal");
354
- if (!strLit)
505
+ const swiftLit = node.namedChildren.find((c) => c.type === "line_string_literal" || c.type === "multi_line_string_literal");
506
+ if (!swiftLit)
355
507
  return;
356
- const literal = extractStringLiteral(strLit);
508
+ const literal = extractStringLiteral(swiftLit);
357
509
  if (literal !== undefined)
358
510
  result = literal;
359
511
  }