slidemind 0.3.20 → 0.4.1

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 (187) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +592 -16
  4. package/LICENSE +4 -0
  5. package/README.md +203 -8
  6. package/THIRD-PARTY-NOTICES.md +201 -0
  7. package/bin/slidemind.mjs +50 -2
  8. package/package.json +5 -3
  9. package/skills/create-slides/SKILL.md +484 -886
  10. package/skills/search/SKILL.md +46 -23
  11. package/src/bridge/build-and-push.mjs +548 -73
  12. package/src/bridge/bundle.mjs +100 -31
  13. package/src/bridge/capture.mjs +14 -17
  14. package/src/bridge/client/brands.mjs +157 -18
  15. package/src/bridge/client/engine-routing.mjs +48 -13
  16. package/src/bridge/client/engine-search.mjs +24 -1
  17. package/src/bridge/client/render.mjs +27 -1
  18. package/src/bridge/client/transport.mjs +499 -23
  19. package/src/bridge/client.mjs +10 -0
  20. package/src/bridge/config.mjs +67 -6
  21. package/src/bridge/engine-download.mjs +0 -10
  22. package/src/bridge/graph-auth.mjs +55 -3
  23. package/src/bridge/image-intent-envelope.mjs +239 -0
  24. package/src/bridge/package-preview.mjs +67 -0
  25. package/src/bridge/sharepoint.mjs +914 -0
  26. package/src/builders/asset-fetch.mjs +203 -0
  27. package/src/builders/block-dividers.mjs +83 -0
  28. package/src/builders/build.mjs +171 -87
  29. package/src/builders/card-height.mjs +294 -0
  30. package/src/builders/chart-hole.mjs +183 -0
  31. package/src/builders/chart-jsx.mjs +410 -0
  32. package/src/builders/chart-labels.mjs +326 -0
  33. package/src/builders/concept-geometry.mjs +370 -0
  34. package/src/builders/concept-intent.mjs +200 -0
  35. package/src/builders/concept-layout.mjs +79 -0
  36. package/src/builders/concept-lines.mjs +509 -0
  37. package/src/builders/concept-lists.mjs +270 -0
  38. package/src/builders/concept-marks.mjs +84 -0
  39. package/src/builders/concept-relationships.mjs +12 -0
  40. package/src/builders/concept-spec.mjs +645 -0
  41. package/src/builders/concept-text.mjs +140 -0
  42. package/src/builders/concept-waves.mjs +57 -0
  43. package/src/builders/copy-fit.mjs +139 -0
  44. package/src/builders/design-tokens.mjs +347 -0
  45. package/src/builders/draft-check.mjs +552 -340
  46. package/src/builders/draft-lines.mjs +369 -0
  47. package/src/builders/edit-route.mjs +29 -0
  48. package/src/builders/figure-sketch.mjs +375 -0
  49. package/src/builders/fold-sets.mjs +639 -0
  50. package/src/builders/gate-server.mjs +9 -4
  51. package/src/builders/gate-session.js +273 -12
  52. package/src/builders/gate-shell.js +1494 -0
  53. package/src/builders/image-intent-check.mjs +308 -0
  54. package/src/builders/image-use-reconcile.mjs +107 -0
  55. package/src/builders/kit-helpers.mjs +121 -0
  56. package/src/builders/layout-names.mjs +32 -0
  57. package/src/builders/legacy-reference-uses.mjs +179 -0
  58. package/src/builders/line-fit.mjs +224 -0
  59. package/src/builders/line-native.mjs +895 -0
  60. package/src/builders/line-relations.mjs +230 -0
  61. package/src/builders/lint-fix.mjs +117 -9
  62. package/src/builders/lint.mjs +25 -20
  63. package/src/builders/list-jsx.mjs +88 -0
  64. package/src/builders/placement.mjs +133 -0
  65. package/src/builders/plan-checks.mjs +135 -0
  66. package/src/builders/plan-comments.mjs +19 -0
  67. package/src/builders/plan-handlers.mjs +95 -0
  68. package/src/builders/plan-markdown-edit.mjs +3 -3
  69. package/src/builders/plan-overlay.js +1530 -1608
  70. package/src/builders/plan.mjs +689 -186
  71. package/src/builders/preset-definitions.json +1 -0
  72. package/src/builders/preset-eval.mjs +244 -0
  73. package/src/builders/preview.html +51 -120
  74. package/src/builders/red-thread-backbone.mjs +135 -36
  75. package/src/builders/repair.mjs +205 -0
  76. package/src/builders/repeat-groups.mjs +540 -0
  77. package/src/builders/resolve-skill.mjs +5 -4
  78. package/src/builders/review-overlay.js +296 -293
  79. package/src/builders/review.mjs +305 -84
  80. package/src/builders/rule-shift.mjs +96 -0
  81. package/src/builders/seat.mjs +354 -0
  82. package/src/builders/settle-driver.mjs +236 -0
  83. package/src/builders/settle.mjs +1448 -0
  84. package/src/builders/slide-prep.mjs +67 -0
  85. package/src/builders/spec-assets.mjs +236 -0
  86. package/src/builders/spec-match.mjs +56 -0
  87. package/src/builders/stack.mjs +413 -0
  88. package/src/builders/storyboard-markdown.mjs +70 -27
  89. package/src/builders/storyboard-schema.mjs +54 -114
  90. package/src/builders/text-blocks.mjs +212 -0
  91. package/src/builders/text-card-fit.mjs +552 -0
  92. package/src/builders/text-stack.mjs +276 -0
  93. package/src/builders/transcribe.mjs +1069 -0
  94. package/src/cli/assets.mjs +164 -0
  95. package/src/cli/common.mjs +173 -12
  96. package/src/cli/concept-spec-support.mjs +76 -0
  97. package/src/cli/concepts.mjs +1002 -103
  98. package/src/cli/deck.mjs +401 -65
  99. package/src/cli/diagnose.mjs +4 -2
  100. package/src/cli/engine-helpers.mjs +17 -10
  101. package/src/cli/engine-results.mjs +76 -3
  102. package/src/cli/help.mjs +232 -28
  103. package/src/cli/install.mjs +9 -0
  104. package/src/cli/maintenance.mjs +104 -0
  105. package/src/cli/read.mjs +2 -1
  106. package/src/cli/ready.mjs +6 -3
  107. package/src/cli/record.mjs +522 -0
  108. package/src/cli/remote.mjs +818 -0
  109. package/src/cli/revision.mjs +686 -0
  110. package/src/cli/search.mjs +30 -11
  111. package/src/cli/session.mjs +56 -13
  112. package/src/cli/setup.mjs +46 -2
  113. package/src/cli/status.mjs +142 -33
  114. package/src/cli/tasks.mjs +246 -0
  115. package/src/cli/transcribe.mjs +854 -0
  116. package/src/cli/workspace-ui.mjs +260 -0
  117. package/src/cli/workspace.mjs +212 -34
  118. package/src/install-channel.mjs +64 -7
  119. package/src/lib/adopt.mjs +346 -25
  120. package/src/lib/concept-spec-binding.mjs +96 -0
  121. package/src/lib/conversions.mjs +70 -6
  122. package/src/lib/convert-folded.mjs +107 -0
  123. package/src/lib/deck-paths.mjs +418 -0
  124. package/src/lib/durable-ids.mjs +47 -0
  125. package/src/lib/edit-operations.mjs +316 -0
  126. package/src/lib/edit-state.mjs +1244 -0
  127. package/src/lib/edit-transitions.mjs +120 -0
  128. package/src/lib/extensions.mjs +501 -0
  129. package/src/lib/image-assets.mjs +465 -0
  130. package/src/lib/image-derivatives.mjs +234 -0
  131. package/src/lib/image-intent-fingerprint.mjs +70 -0
  132. package/src/lib/image-intent-tags.mjs +176 -0
  133. package/src/lib/image-intent.mjs +802 -0
  134. package/src/lib/image-use-overrides.mjs +279 -0
  135. package/src/lib/inline-components.mjs +613 -0
  136. package/src/lib/lint-converted.mjs +68 -4
  137. package/src/lib/merge-json.mjs +163 -0
  138. package/src/lib/overwrite-guard.mjs +3 -3
  139. package/src/lib/presentation-context.mjs +531 -0
  140. package/src/lib/preservation-tags.mjs +157 -0
  141. package/src/lib/preservation-verdict.mjs +1342 -0
  142. package/src/lib/publish.mjs +1557 -0
  143. package/src/lib/rebase.mjs +945 -0
  144. package/src/lib/reconcile-ack.mjs +4 -6
  145. package/src/lib/reconcile.mjs +62 -24
  146. package/src/lib/record-client.mjs +175 -0
  147. package/src/lib/record-state.mjs +151 -0
  148. package/src/lib/remote.mjs +661 -0
  149. package/src/lib/revision.mjs +1210 -0
  150. package/src/lib/slide-guids.mjs +4 -6
  151. package/src/lib/slide-preservation.mjs +423 -0
  152. package/src/lib/slide-selection.mjs +9 -0
  153. package/src/lib/workspace-transaction.mjs +117 -0
  154. package/src/pptx/content-digest.mjs +334 -0
  155. package/src/pptx/deck-assemble.mjs +1278 -0
  156. package/src/pptx/fill/engine.mjs +2 -3
  157. package/src/pptx/fill/freeform.mjs +6 -3
  158. package/src/pptx/ooxml.mjs +20 -0
  159. package/src/pptx/package-evidence.mjs +383 -0
  160. package/src/pptx/tags.mjs +163 -11
  161. package/src/update-check.mjs +22 -4
  162. package/src/workspace/adapters.mjs +222 -0
  163. package/src/workspace/auth.mjs +256 -0
  164. package/src/workspace/autostart.mjs +204 -0
  165. package/src/workspace/daemon.mjs +360 -0
  166. package/src/workspace/discovery.mjs +180 -0
  167. package/src/workspace/edit.mjs +167 -0
  168. package/src/workspace/events.mjs +52 -0
  169. package/src/workspace/gate-client.mjs +290 -0
  170. package/src/workspace/handback.mjs +73 -0
  171. package/src/workspace/lifecycle.mjs +635 -0
  172. package/src/workspace/operations.mjs +200 -0
  173. package/src/workspace/pick-folder.mjs +152 -0
  174. package/src/workspace/registry.mjs +359 -0
  175. package/src/workspace/remote-links.mjs +397 -0
  176. package/src/workspace/remote-status.mjs +359 -0
  177. package/src/workspace/routes.mjs +1331 -0
  178. package/src/workspace/server.mjs +159 -0
  179. package/src/workspace/sync.mjs +1119 -0
  180. package/src/workspace/task-actions.mjs +215 -0
  181. package/src/workspace/tasks.mjs +507 -0
  182. package/src/workspace/ui/app.js +1886 -0
  183. package/src/workspace/ui/index.html +141 -0
  184. package/src/workspace/ui/plan-bridge.js +120 -0
  185. package/src/workspace/ui/sharepoint.svg +59 -0
  186. package/src/workspace/ui/styles.css +423 -0
  187. package/src/builders/visual-families.mjs +0 -46
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { deriveEnvLabel } from "../config.mjs";
25
25
  import { getDesignBundleForBrand, getDesignBundleForBrandAnyBackend } from "./brands.mjs";
26
- import { isSessionInvalidError, sessionsInScope, bridgeFetch, backendCapabilitiesResult, backendSupportsEngineRoutes } from "./transport.mjs";
26
+ import { isSessionInvalidError, sessionsInScope, bridgeFetch, backendCapabilitiesResult, backendSupportsEngineRoutes, capabilityUnknownError } from "./transport.mjs";
27
27
 
28
28
  export function searchRoute(searchId, suffix = "") {
29
29
  return `/api/cli/searches/${encodeURIComponent(String(searchId))}${suffix}`;
@@ -86,11 +86,12 @@ export async function resolveSearchBackend(searchId) {
86
86
  if (!isOpaqueSearchId(searchId)) return null;
87
87
  const failures = [];
88
88
  for (const s of sessionsInScope()) {
89
- // A revoked session 401s the capability probe too, and that answer is
90
- // indistinguishable from "old add-in" unless the failure is kept.
89
+ // A probe that got no answer (a revoked session 401s it; a slow relay
90
+ // times it out) says nothing about whether THIS environment holds the
91
+ // record — keep the failure, so "search expired" is never said for it.
91
92
  const probe = await backendCapabilitiesResult(s.backendURL);
92
- if (probe.error && isSessionInvalidError(probe.error)) {
93
- failures.push(probe.error);
93
+ if (probe.status === "unknown") {
94
+ failures.push(capabilityUnknownError(s.backendURL, probe));
94
95
  continue;
95
96
  }
96
97
  if (probe.caps?.engineRoutes !== true) continue;
@@ -112,6 +113,10 @@ export async function resolveSearchBackend(searchId) {
112
113
  (e) => isSessionInvalidError(e) || e?.code === "login_required",
113
114
  );
114
115
  if (blocking) throw blocking;
116
+ // An environment whose capabilities could not be read may well hold the
117
+ // record: that is a retryable failure, not an expiry.
118
+ const unknown = failures.find((e) => e?.code === "capability_unknown");
119
+ if (unknown) throw unknown;
115
120
  return null;
116
121
  }
117
122
 
@@ -126,17 +131,37 @@ export async function resolveSearchBackend(searchId) {
126
131
  // the design does the caller fall back to the pane relay (the design lives on
127
132
  // an older add-in, and searching a different corpus would fold ITS slides into
128
133
  // this deck). Returns null for that fallback and when nothing paired serves the
129
- // routes at all.
134
+ // routes at all — and only when that is KNOWN: an environment whose capability
135
+ // probe got no answer throws `capability_unknown` (retryable, see
136
+ // transport.backendCapabilities) rather than being counted as an old add-in.
137
+ // The pane relay is the path for an add-in without the routes, never for a
138
+ // request that timed out (x103).
139
+ //
140
+ // That unanswered probe is thrown only when the answer DEPENDS on it. A
141
+ // candidate that serves the deck's design wins outright whatever the silent
142
+ // environment would have said, so a stopped `local` must not block a deck
143
+ // bound to `dev` (2026-10-11). Where it could have changed the pick (no
144
+ // candidate at all, no candidate serving the design, a sole candidate that
145
+ // might have had a rival), guessing would search another corpus, so it throws.
130
146
  export async function resolveEngineBackend({ backendURL = null, brandId = null } = {}) {
131
147
  if (backendURL) {
132
148
  return (await backendSupportsEngineRoutes(backendURL)) ? backendURL : null;
133
149
  }
134
150
  const sessions = sessionsInScope();
135
151
  const candidates = [];
152
+ const unanswered = [];
136
153
  for (const s of sessions) {
137
- if (await backendSupportsEngineRoutes(s.backendURL)) candidates.push(s.backendURL);
154
+ try {
155
+ if (await backendSupportsEngineRoutes(s.backendURL)) candidates.push(s.backendURL);
156
+ } catch (err) {
157
+ unanswered.push({ backendURL: s.backendURL, err });
158
+ }
138
159
  }
139
- if (candidates.length === 0) return null;
160
+ const unlessUnanswered = (answer) => {
161
+ if (unanswered.length) throw unanswered[0].err;
162
+ return answer;
163
+ };
164
+ if (candidates.length === 0) return unlessUnanswered(null);
140
165
  // Exactly one CONNECTED environment: there is nothing else the deck could
141
166
  // belong to. Note the test is on the sessions, not on the candidates — during
142
167
  // a rollout only one of several environments serves the routes, and taking it
@@ -145,7 +170,7 @@ export async function resolveEngineBackend({ backendURL = null, brandId = null }
145
170
  if (brandId) {
146
171
  // The memo only stands while that environment is still a candidate.
147
172
  const memo = engineBackendForBrand.get(brandId);
148
- if (memo) return candidates.includes(memo) ? memo : null;
173
+ if (memo) return candidates.includes(memo) ? memo : unlessUnanswered(null);
149
174
  // Candidates first: an environment that serves both the design AND the
150
175
  // engine routes wins outright, even when an older environment also has the
151
176
  // (content-identical) design.
@@ -158,6 +183,8 @@ export async function resolveEngineBackend({ backendURL = null, brandId = null }
158
183
  // This candidate doesn't serve the design — try the next.
159
184
  }
160
185
  }
186
+ // The silent environment may be the candidate that serves it.
187
+ unlessUnanswered();
161
188
  try {
162
189
  const { backendURL: be } = await getDesignBundleForBrandAnyBackend(brandId);
163
190
  engineBackendForBrand.set(brandId, be);
@@ -170,7 +197,7 @@ export async function resolveEngineBackend({ backendURL = null, brandId = null }
170
197
  // rather than guessing an environment.
171
198
  }
172
199
  }
173
- if (candidates.length === 1) return candidates[0];
200
+ if (candidates.length === 1) return unlessUnanswered(candidates[0]);
174
201
  const err = new Error(
175
202
  `Several connected environments run engine search (${candidates
176
203
  .map((u) => deriveEnvLabel(u))
@@ -186,16 +213,24 @@ export async function resolveEngineBackend({ backendURL = null, brandId = null }
186
213
  // "backend authoring ready" half of the readiness picture (`doctor`, `ready`),
187
214
  // reported as ENV LABELS so no URL can reach a printed line. Pass
188
215
  // connectionStatus()'s `backends` to skip environments that are already known
189
- // unreachable; without it every session in scope is probed.
216
+ // unreachable; without it every session in scope is probed. An environment
217
+ // whose probe got no answer is listed under `unknown`, not under "no routes":
218
+ // a readiness report must not call a slow relay an old add-in.
190
219
  export async function engineReachability({ backends = null } = {}) {
191
220
  const urls = Array.isArray(backends)
192
221
  ? backends.filter((b) => b?.ok && b.backendURL).map((b) => b.backendURL)
193
222
  : sessionsInScope().map((s) => s.backendURL);
194
223
  const envs = [];
224
+ const unknown = [];
195
225
  for (const url of urls) {
196
- if (await backendSupportsEngineRoutes(url)) envs.push(deriveEnvLabel(url));
226
+ try {
227
+ if (await backendSupportsEngineRoutes(url)) envs.push(deriveEnvLabel(url));
228
+ } catch (err) {
229
+ if (err?.code !== "capability_unknown") throw err;
230
+ unknown.push(deriveEnvLabel(url));
231
+ }
197
232
  }
198
- return { available: envs.length > 0, envs };
233
+ return { available: envs.length > 0, envs, unknown };
199
234
  }
200
235
 
201
236
  // Resolve + verify the backend for a direct engine call. Throws a typed
@@ -4,7 +4,7 @@ import { deriveEnvLabel } from "../config.mjs";
4
4
  import { assertSearchId, downloadTimeoutMs, resultRoute, directEngineBackend } from "./engine-routing.mjs";
5
5
  import { pickAnyPresentationEntry } from "./pane-ops.mjs";
6
6
  import { resolveBackendForPres, engineOpWithPaneWait } from "./presentations.mjs";
7
- import { engineTimeoutMs, bridgeFetch, bridgeStreamToFile, envInt, delegatedBodyFields, graphHeaders } from "./transport.mjs";
7
+ import { engineTimeoutMs, bridgeFetch, bridgeStreamToFile, envInt, delegatedBodyFields, graphHeaders, backendCapabilities } from "./transport.mjs";
8
8
 
9
9
  // Brand-engine search through the local bridge. New add-ins run this
10
10
  // server-side with the relayed user token; older add-ins may fall back to a
@@ -308,3 +308,26 @@ export async function engineDownloadToFile({
308
308
  return shape(res);
309
309
  });
310
310
  }
311
+
312
+ // The collage reader behind `transcribe`'s icon fetch: each item is a motif plus
313
+ // the numbered contact sheet (`gridDataUri`) its search returned; the answer is
314
+ // one 1-based selection, or null for "nothing here shows it", per item. Null
315
+ // when the backend does not offer it (no `assetPicks` capability) — the caller
316
+ // then decides by file name alone.
317
+ export async function pickAssets({ items, backendURL }) {
318
+ const caps = await backendCapabilities(backendURL);
319
+ if (caps?.assetPicks !== true) return null;
320
+ if (!items.length) return [];
321
+ const res = await bridgeFetch(
322
+ "/api/cli/asset-picks",
323
+ {
324
+ method: "POST",
325
+ headers: { "content-type": "application/json" },
326
+ body: JSON.stringify({ items }),
327
+ timeoutMs: engineTimeoutMs(),
328
+ },
329
+ backendURL,
330
+ );
331
+ const data = (await res.json()) ?? {};
332
+ return Array.isArray(data.picks) ? data.picks : null;
333
+ }
@@ -9,7 +9,14 @@
9
9
  // for older backends that don't serve /api/cli/capabilities yet.
10
10
 
11
11
  import { resolveBackendForPres } from "./presentations.mjs";
12
- import { bridgeFetch, envInt, sleep, backendSupportsRenderJobs, assertCliVersionSupported } from "./transport.mjs";
12
+ import {
13
+ bridgeFetch,
14
+ envInt,
15
+ sleep,
16
+ backendSupportsRenderJobs,
17
+ assertCliVersionSupported,
18
+ assertImageIntentSupported,
19
+ } from "./transport.mjs";
13
20
 
14
21
  // Rebuild a job failure into the exact error shape bridgeFetch throws for the
15
22
  // sync route, so callers (etag self-heal, error texts) can't tell the
@@ -197,6 +204,7 @@ export async function renderPptx({
197
204
  sourceGuids = null,
198
205
  conversions = null,
199
206
  reconcile = null,
207
+ imageIntent = null,
200
208
  backendURL = null,
201
209
  onProgress = null,
202
210
  }) {
@@ -217,6 +225,10 @@ export async function renderPptx({
217
225
  // speaks this CLI's contract fails deep and confusingly; fail here instead
218
226
  // with the actionable `slidemind update`.
219
227
  await assertCliVersionSupported(be);
228
+ // And, when this deck carries approved image obligations, the contract that
229
+ // enforces them. Before the upload, so an old add-in costs a capability probe
230
+ // rather than a render that silently drops the user's picture.
231
+ if (imageIntent) await assertImageIntentSupported(be);
220
232
  const basePath = renderBasePath({ presId, brandId });
221
233
  const label = brandId ? (deckName ?? brandId) : presId;
222
234
  const body = { slides: slides ?? {} };
@@ -251,6 +263,14 @@ export async function renderPptx({
251
263
  if (reconcile && Object.keys(reconcile).length > 0) {
252
264
  body.reconcile = reconcile;
253
265
  }
266
+ // The deck's approved image obligations (src/bridge/image-intent-envelope.mjs).
267
+ // Its asset descriptors carry `workspacePath`, not bytes: those bytes are
268
+ // already in `deckAssets` above under that exact key, so the envelope points
269
+ // at them instead of shipping every screenshot twice. It also travels when the
270
+ // JSX forgot to bind a use — that is the whole point of validating against
271
+ // expectations rather than against the JSX. Additive: a deck with no image
272
+ // uses sends the same body it always did.
273
+ if (imageIntent) body.imageIntent = imageIntent;
254
274
  // The brand surface has no presId to label the render with — pass the deck
255
275
  // slug so job records/temp dirs stay readable (server-side sanitized).
256
276
  if (brandId && deckName) body.deckName = deckName;
@@ -278,6 +298,7 @@ export async function translateDeck({
278
298
  captured,
279
299
  nativeSlides = null,
280
300
  etag = null,
301
+ imageIntent = null,
281
302
  backendURL = null,
282
303
  }) {
283
304
  if (!presId && !brandId) {
@@ -290,6 +311,7 @@ export async function translateDeck({
290
311
  be = await resolveBackendForPres(presId);
291
312
  }
292
313
  await assertCliVersionSupported(be);
314
+ if (imageIntent) await assertImageIntentSupported(be);
293
315
  const body = { captured };
294
316
  // Reused native one-slide .pptx (base64), keyed by play-order filename — the
295
317
  // server splices them in alongside the locally-captured JSX slides (contract
@@ -298,6 +320,10 @@ export async function translateDeck({
298
320
  body.nativeSlides = nativeSlides;
299
321
  }
300
322
  if (etag) body.etag = etag;
323
+ // Same envelope as the render route: expected image obligations, descriptors
324
+ // pointing into the captured deck's own asset bytes. Both routes have to
325
+ // enforce the same thing, so both carry the same expectations.
326
+ if (imageIntent) body.imageIntent = imageIntent;
301
327
  if (brandId && deckName) body.deckName = deckName;
302
328
  const res = await bridgeFetch(
303
329
  `${renderBasePath({ presId, brandId })}/translate`,