@remnic/core 9.3.685 → 9.3.686

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 (55) hide show
  1. package/dist/access-boundary.d.ts +2 -2
  2. package/dist/access-boundary.js +2 -2
  3. package/dist/access-cli.js +88 -7
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +1 -1
  6. package/dist/access-http.js +5 -5
  7. package/dist/access-mcp.d.ts +12 -2
  8. package/dist/access-mcp.js +4 -4
  9. package/dist/access-operations.d.ts +8 -3
  10. package/dist/access-operations.js +5 -3
  11. package/dist/access-schema.d.ts +4 -4
  12. package/dist/{access-service-DeKrlYU_.d.ts → access-service-DmCHJ4cH.d.ts} +105 -29
  13. package/dist/access-service.d.ts +1 -1
  14. package/dist/access-service.js +1 -1
  15. package/dist/access-surface-catalog.d.ts +1 -1
  16. package/dist/access-surface-catalog.js +2 -0
  17. package/dist/access-surface-catalog.js.map +1 -1
  18. package/dist/{chunk-OFUULUSY.js → chunk-473JIN2U.js} +56 -5
  19. package/dist/chunk-473JIN2U.js.map +1 -0
  20. package/dist/{chunk-SQGPGC76.js → chunk-FUCUR2OZ.js} +540 -43
  21. package/dist/chunk-FUCUR2OZ.js.map +1 -0
  22. package/dist/{chunk-IIDSFFE5.js → chunk-KFBOZYME.js} +42 -3
  23. package/dist/chunk-KFBOZYME.js.map +1 -0
  24. package/dist/{chunk-PK6RGRSD.js → chunk-NN7QYW5W.js} +2 -2
  25. package/dist/chunk-NN7QYW5W.js.map +1 -0
  26. package/dist/{chunk-JPCKLFWK.js → chunk-QVMXQGT7.js} +6 -5
  27. package/dist/chunk-QVMXQGT7.js.map +1 -0
  28. package/dist/{chunk-BZISAF67.js → chunk-S2OU5DZY.js} +28 -6
  29. package/dist/chunk-S2OU5DZY.js.map +1 -0
  30. package/dist/{cli-D3-Q5Uod.d.ts → cli-D8nZ2MPH.d.ts} +1 -1
  31. package/dist/cli.d.ts +2 -2
  32. package/dist/cli.js +6 -6
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +6 -6
  35. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  36. package/dist/schemas.d.ts +38 -38
  37. package/dist/transfer/types.d.ts +22 -22
  38. package/package.json +2 -2
  39. package/src/access-boundary.ts +2 -1
  40. package/src/access-cli.ts +94 -4
  41. package/src/access-http.ts +39 -1
  42. package/src/access-mcp.ts +54 -1
  43. package/src/access-operations.ts +66 -0
  44. package/src/access-service.ts +147 -62
  45. package/src/access-surface-catalog.test.ts +1 -1
  46. package/src/access-surface-catalog.ts +2 -0
  47. package/src/cli.ts +1 -0
  48. package/src/coding/decision-surfaces.test.ts +279 -0
  49. package/src/coding/decision-surfaces.ts +475 -0
  50. package/dist/chunk-BZISAF67.js.map +0 -1
  51. package/dist/chunk-IIDSFFE5.js.map +0 -1
  52. package/dist/chunk-JPCKLFWK.js.map +0 -1
  53. package/dist/chunk-OFUULUSY.js.map +0 -1
  54. package/dist/chunk-PK6RGRSD.js.map +0 -1
  55. package/dist/chunk-SQGPGC76.js.map +0 -1
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Decision-record surface contract + handler (issue #1548 Track A PR 2).
3
+ *
4
+ * Rule 39: one gate predicate, checked identically on every surface. Rule 22
5
+ * spirit: one implementation behind three thin wirings. The service holds
6
+ * only a thin delegate that builds a {@link DecisionSurfaceContext} and calls
7
+ * {@link handleCodingDecision}; the handler logic lives here so the
8
+ * access-service god file gains thin wiring only.
9
+ *
10
+ * No orchestrator imports (rule 11 — no shared mutable state). No circular
11
+ * dependency on access-service.ts: validation errors are thrown via
12
+ * `ctx.throwInputError`, which the service wires to EngramAccessInputError.
13
+ */
14
+ import type { CodingKnowledgeConfig, CodingContext, MemoryFile, MemoryFrontmatter, MemoryStatus } from "../types.js";
15
+ import {
16
+ ACTIVE_DECISION_STATUSES,
17
+ DEFAULT_DECISION_STATUS,
18
+ isDecisionStatus,
19
+ parseDecisionRecord,
20
+ serializeDecisionRecord,
21
+ type DecisionRecord,
22
+ type DecisionStatus,
23
+ } from "./decision-records.js";
24
+ import { log } from "../logger.js";
25
+
26
+ // ──────────────────────────────────────────────────────────────────────────
27
+ // Subcommands
28
+ // ──────────────────────────────────────────────────────────────────────────
29
+
30
+ export const DECISION_SUBCOMMANDS = [
31
+ "list",
32
+ "get",
33
+ "record",
34
+ "supersede",
35
+ ] as const;
36
+
37
+ export type DecisionSubcommand = (typeof DECISION_SUBCOMMANDS)[number];
38
+
39
+ const SUBCOMMAND_VALUES = DECISION_SUBCOMMANDS as readonly string[];
40
+
41
+ /**
42
+ * Type guard — narrows an unknown subcommand string to the
43
+ * {@link DecisionSubcommand} union.
44
+ */
45
+ export function isDecisionSubcommand(value: unknown): value is DecisionSubcommand {
46
+ return typeof value === "string" && SUBCOMMAND_VALUES.includes(value);
47
+ }
48
+
49
+ /**
50
+ * Human-readable subcommand list for error messages (rule 51 — list valid
51
+ * options so the caller can correct rather than guess).
52
+ */
53
+ export function formatDecisionSubcommands(): string {
54
+ return DECISION_SUBCOMMANDS.join(", ");
55
+ }
56
+
57
+ // ──────────────────────────────────────────────────────────────────────────
58
+ // Gate predicate — rule 39: one predicate, identical on every surface
59
+ // ──────────────────────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * The single decision-record surface gate. Returns `true` only when:
63
+ * 1. `codingKnowledge.enabled` is on (the master Track A gate),
64
+ * 2. `codingKnowledge.decisionRecords` is on (the feature switch), AND
65
+ * 3. A coding context is attached (the session is project/branch scoped —
66
+ * decision records live *in* the coding namespace, rule 42).
67
+ *
68
+ * Every surface — MCP `engram.coding_decision`, HTTP
69
+ * `POST /engram/v1/coding/decisions`, CLI `engram-access decision` — MUST call
70
+ * this predicate (or the handler that embeds it) before dispatching. The
71
+ * tool-visibility gate in the MCP constructor checks conditions 1–2 only
72
+ * (coding context is per-session and cannot be evaluated at construction
73
+ * time); the call-time gate checks all three.
74
+ */
75
+ export function isDecisionRecordSurfaceEnabled(
76
+ config: CodingKnowledgeConfig,
77
+ codingContext: CodingContext | null | undefined,
78
+ ): boolean {
79
+ return (
80
+ config.enabled === true &&
81
+ config.decisionRecords === true &&
82
+ codingContext != null
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Config-only visibility gate — used by the MCP constructor to decide whether
88
+ * to advertise `engram.coding_decision` in `tools/list`. When this returns
89
+ * `false` the tools array is byte-identical to pre-feature (rule 39).
90
+ */
91
+ export function isDecisionRecordSurfaceVisible(
92
+ config: CodingKnowledgeConfig,
93
+ ): boolean {
94
+ return config.enabled === true && config.decisionRecords === true;
95
+ }
96
+
97
+ // ──────────────────────────────────────────────────────────────────────────
98
+ // Surface request / response shapes
99
+ // ──────────────────────────────────────────────────────────────────────────
100
+
101
+ /**
102
+ * Canonical surface request — one shape for all three transports. The
103
+ * `subcommand` field selects which operation runs; the remaining fields are
104
+ * optional depending on the subcommand.
105
+ *
106
+ * `sessionKey` identifies the session whose coding context scopes the
107
+ * operation. `namespace` overrides the coding-scoped namespace (same
108
+ * precedence as `memory_store` — explicit namespace wins).
109
+ */
110
+ export interface DecisionSurfaceRequest {
111
+ subcommand: DecisionSubcommand;
112
+ sessionKey?: string;
113
+ namespace?: string;
114
+ // get / supersede
115
+ id?: string;
116
+ // record
117
+ title?: string;
118
+ status?: string;
119
+ context?: string;
120
+ decision?: string;
121
+ consequences?: string;
122
+ entityRefs?: string[];
123
+ // supersede
124
+ supersedesId?: string;
125
+ }
126
+
127
+ /**
128
+ * Surface response — a discriminated union on `subcommand`. Each surface
129
+ * serializes this to its transport-appropriate shape.
130
+ */
131
+ export type DecisionSurfaceResponse =
132
+ | { subcommand: "list"; records: DecisionSurfaceRecord[]; count: number }
133
+ | { subcommand: "get"; found: boolean; record?: DecisionSurfaceRecord }
134
+ | { subcommand: "record"; memoryId: string; status: string }
135
+ | {
136
+ subcommand: "supersede";
137
+ supersededMemoryId: string;
138
+ replacementMemoryId: string;
139
+ };
140
+
141
+ /**
142
+ * Flattened record projection surfaced to clients. Stored as markdown +
143
+ * frontmatter memory files (category `"decision"`) — this shape is the
144
+ * read-side projection, not the storage format.
145
+ */
146
+ export interface DecisionSurfaceRecord {
147
+ id: string;
148
+ title: string;
149
+ status: string;
150
+ context?: string;
151
+ decision?: string;
152
+ consequences?: string;
153
+ entityRefs: string[];
154
+ supersedes?: string;
155
+ }
156
+
157
+ // ──────────────────────────────────────────────────────────────────────────
158
+ // Handler — the single implementation behind all three surfaces (rule 22)
159
+ // ──────────────────────────────────────────────────────────────────────────
160
+
161
+ /**
162
+ * Structural subset of StorageManager the decision handler reads or writes.
163
+ * Kept narrow so the module stays decoupled from storage.ts and is
164
+ * unit-testable with a stub.
165
+ */
166
+ export interface DecisionSurfaceStorage {
167
+ readonly dir: string;
168
+ /** The resolved namespace — used for catalog write recording. */
169
+ readonly namespace: string;
170
+ readAllMemories(): Promise<readonly MemoryFile[]>;
171
+ getMemoryById(id: string): Promise<MemoryFile | null>;
172
+ writeMemory(
173
+ category: "decision",
174
+ content: string,
175
+ options: {
176
+ confidence?: number;
177
+ tags?: string[];
178
+ source?: string;
179
+ /** Outer memory lifecycle status — set to "archived" for inactive
180
+ * decisions so generic recall/search/maintenance exclude them
181
+ * (review P2: persist inactive decision statuses in frontmatter). */
182
+ status?: MemoryStatus;
183
+ /** Decision-specific lifecycle marker, mirrored from the serialized
184
+ * body so the list/get projection has one authoritative source. */
185
+ structuredAttributes?: Record<string, string>;
186
+ },
187
+ ): Promise<string>;
188
+ writeMemoryFrontmatter(
189
+ memory: MemoryFile,
190
+ patch: Partial<MemoryFrontmatter>,
191
+ ): Promise<unknown>;
192
+ }
193
+
194
+ /**
195
+ * Dependencies the handler borrows from the service. The service constructs
196
+ * this context per call; the handler never touches the orchestrator directly.
197
+ * `throwInputError` lets the handler raise the surface-appropriate error
198
+ * class without importing access-service.ts (no circular dependency).
199
+ */
200
+ export interface DecisionSurfaceContext {
201
+ readonly codingKnowledge: CodingKnowledgeConfig;
202
+ getCodingContext(sessionKey: string): CodingContext | null;
203
+ /** Resolve storage through the SAME namespace path as memory_store
204
+ * (principal ACL + coding overlay + default fallback). The #1522 storage
205
+ * chokepoint records the catalog write automatically on every
206
+ * storage.writeMemory, so the handler does NOT touch the catalog itself. */
207
+ resolveStorage(request: DecisionSurfaceRequest): Promise<DecisionSurfaceStorage>;
208
+ /** Throw the surface-appropriate input-validation error. */
209
+ throwInputError(message: string): never;
210
+ }
211
+
212
+ /**
213
+ * The single shared implementation behind the MCP, HTTP, and CLI
214
+ * decision-record surfaces. All three transports dispatch through the
215
+ * `coding_decision` boundary operation, which calls this function via the
216
+ * service delegate.
217
+ *
218
+ * Gate (rule 39): `codingKnowledge.enabled + decisionRecords + coding
219
+ * context`. Persistence (rule 43): records are written through the storage
220
+ * manager's normal persist pipeline with category `"decision"` — no direct
221
+ * `fs` writes of memory content. Supersede (rule 25): the replacement is
222
+ * written BEFORE the old record's `structuredAttributes.decisionStatus` is
223
+ * set to `"superseded"` — the structuredAttribute is the authoritative
224
+ * lifecycle marker; content is never rewritten.
225
+ */
226
+ export async function handleCodingDecision(
227
+ request: DecisionSurfaceRequest,
228
+ ctx: DecisionSurfaceContext,
229
+ ): Promise<DecisionSurfaceResponse> {
230
+ const codingContext = request.sessionKey
231
+ ? ctx.getCodingContext(request.sessionKey)
232
+ : null;
233
+ if (!isDecisionRecordSurfaceEnabled(ctx.codingKnowledge, codingContext)) {
234
+ ctx.throwInputError(
235
+ "coding_decision requires codingKnowledge.enabled, codingKnowledge.decisionRecords, and an attached coding context",
236
+ );
237
+ }
238
+ switch (request.subcommand) {
239
+ case "list":
240
+ return decisionList(request, ctx);
241
+ case "get":
242
+ return decisionGet(request, ctx);
243
+ case "record":
244
+ return decisionRecord(request, ctx);
245
+ case "supersede":
246
+ return decisionSupersede(request, ctx);
247
+ }
248
+ }
249
+
250
+ async function decisionList(
251
+ request: DecisionSurfaceRequest,
252
+ ctx: DecisionSurfaceContext,
253
+ ): Promise<DecisionSurfaceResponse> {
254
+ const storage = await ctx.resolveStorage(request);
255
+ const memories = await storage.readAllMemories();
256
+ const records: DecisionSurfaceRecord[] = [];
257
+ for (const m of memories) {
258
+ if (m.frontmatter.category !== "decision") continue;
259
+ // Exclude lifecycle-retired memories. Any outer frontmatter.status other
260
+ // than undefined/"active" (archived, superseded, forgotten, rejected,
261
+ // quarantined, pending_review) means the generic memory lifecycle has
262
+ // intervened — hide the decision until that resolves. The decision-specific
263
+ // lifecycle marker lives in structuredAttributes.decisionStatus (review:
264
+ // hide all non-active outer statuses from decisions).
265
+ const memStatus = m.frontmatter.status;
266
+ if (memStatus && memStatus !== "active") continue;
267
+ const parsed = safeParseDecisionRecord(m.content);
268
+ if (!parsed) continue;
269
+ const structStatus = m.frontmatter.structuredAttributes?.decisionStatus;
270
+ const effectiveStatus = structStatus ?? parsed.status;
271
+ records.push({
272
+ id: m.frontmatter.id,
273
+ title: parsed.title,
274
+ status: effectiveStatus,
275
+ entityRefs: parsed.entityRefs,
276
+ supersedes: parsed.supersedes,
277
+ });
278
+ }
279
+ const visible = records.filter((r) =>
280
+ ACTIVE_DECISION_STATUSES.has(r.status as DecisionStatus),
281
+ );
282
+ return { subcommand: "list", records: visible, count: visible.length };
283
+ }
284
+
285
+ async function decisionGet(
286
+ request: DecisionSurfaceRequest,
287
+ ctx: DecisionSurfaceContext,
288
+ ): Promise<DecisionSurfaceResponse> {
289
+ if (!request.id?.trim()) {
290
+ ctx.throwInputError("id is required for the 'get' subcommand");
291
+ }
292
+ const storage = await ctx.resolveStorage(request);
293
+ const memory = await storage.getMemoryById(request.id!);
294
+ if (!memory || memory.frontmatter.category !== "decision") {
295
+ return { subcommand: "get", found: false };
296
+ }
297
+ const parsed = safeParseDecisionRecord(memory.content);
298
+ if (!parsed) {
299
+ return { subcommand: "get", found: false };
300
+ }
301
+ const structStatus = memory.frontmatter.structuredAttributes?.decisionStatus;
302
+ return {
303
+ subcommand: "get",
304
+ found: true,
305
+ record: {
306
+ id: memory.frontmatter.id,
307
+ title: parsed.title,
308
+ status: structStatus ?? parsed.status,
309
+ context: parsed.context,
310
+ decision: parsed.decision,
311
+ consequences: parsed.consequences,
312
+ entityRefs: parsed.entityRefs,
313
+ supersedes: parsed.supersedes,
314
+ },
315
+ };
316
+ }
317
+
318
+ async function decisionRecord(
319
+ request: DecisionSurfaceRequest,
320
+ ctx: DecisionSurfaceContext,
321
+ ): Promise<DecisionSurfaceResponse> {
322
+ if (!request.title?.trim()) {
323
+ ctx.throwInputError("title is required for the 'record' subcommand");
324
+ }
325
+ if (!request.decision?.trim()) {
326
+ ctx.throwInputError("decision is required for the 'record' subcommand");
327
+ }
328
+ const status: DecisionStatus = request.status?.trim()
329
+ ? isDecisionStatus(request.status)
330
+ ? request.status
331
+ : raiseInvalidStatus(request.status, ctx)
332
+ : DEFAULT_DECISION_STATUS;
333
+ const record: DecisionRecord = {
334
+ id: "",
335
+ title: request.title.trim(),
336
+ status,
337
+ context: request.context?.trim() ?? "",
338
+ decision: request.decision.trim(),
339
+ consequences: request.consequences?.trim() ?? "",
340
+ entityRefs: request.entityRefs ?? [],
341
+ };
342
+ const content = serializeDecisionRecord(record);
343
+ const storage = await ctx.resolveStorage(request);
344
+ const isActive = ACTIVE_DECISION_STATUSES.has(status);
345
+ const memoryId = await storage.writeMemory("decision", content, {
346
+ confidence: 1.0,
347
+ tags: ["decision-record"],
348
+ source: "coding-decision",
349
+ // Persist the decision lifecycle in BOTH places so generic
350
+ // recall/search/maintenance (which read frontmatter.status) and the
351
+ // decision list/get projection (which reads structuredAttributes) agree:
352
+ // - structuredAttributes.decisionStatus is the authoritative decision
353
+ // marker, mirrored from the serialized body (one source of truth);
354
+ // - frontmatter.status is set to "archived" for inactive decisions
355
+ // (rejected/superseded) so the outer memory pipeline excludes them
356
+ // from the active corpus exactly like a supersede does (review P2:
357
+ // persist inactive decision statuses in frontmatter).
358
+ structuredAttributes: { decisionStatus: status },
359
+ status: isActive ? undefined : "archived",
360
+ });
361
+ log.info(
362
+ `access-write op=coding_decision/record memoryId=${memoryId} status=${status}`,
363
+ );
364
+ return { subcommand: "record", memoryId, status };
365
+ }
366
+
367
+ async function decisionSupersede(
368
+ request: DecisionSurfaceRequest,
369
+ ctx: DecisionSurfaceContext,
370
+ ): Promise<DecisionSurfaceResponse> {
371
+ // The schema advertises `supersedesId` for MCP/HTTP clients that name it
372
+ // explicitly; treat it as an alias for `id` when `id` is absent (review P2).
373
+ const targetId = request.id?.trim() || request.supersedesId?.trim();
374
+ if (!targetId) {
375
+ ctx.throwInputError(
376
+ "id (or supersedesId) is required for the 'supersede' subcommand (the record being superseded)",
377
+ );
378
+ }
379
+ if (!request.title?.trim()) {
380
+ ctx.throwInputError(
381
+ "title is required for the 'supersede' subcommand (the replacement record)",
382
+ );
383
+ }
384
+ if (!request.decision?.trim()) {
385
+ ctx.throwInputError("decision is required for the 'supersede' subcommand");
386
+ }
387
+ const storage = await ctx.resolveStorage(request);
388
+ const oldMemory = await storage.getMemoryById(targetId);
389
+ if (!oldMemory || oldMemory.frontmatter.category !== "decision") {
390
+ ctx.throwInputError(`decision record not found: ${targetId}`);
391
+ }
392
+ const oldParsed = safeParseDecisionRecord(oldMemory.content);
393
+ if (!oldParsed) {
394
+ ctx.throwInputError(
395
+ `decision record is corrupted and cannot be superseded: ${targetId}`,
396
+ );
397
+ }
398
+ // Rule 25: write the replacement BEFORE mutating the old record's status.
399
+ const replacement: DecisionRecord = {
400
+ id: "",
401
+ title: request.title.trim(),
402
+ status: "accepted",
403
+ context: request.context?.trim() ?? "",
404
+ decision: request.decision.trim(),
405
+ consequences: request.consequences?.trim() ?? "",
406
+ entityRefs: request.entityRefs ?? [],
407
+ supersedes: targetId,
408
+ };
409
+ const replacementContent = serializeDecisionRecord(replacement);
410
+ const replacementId = await storage.writeMemory(
411
+ "decision",
412
+ replacementContent,
413
+ {
414
+ confidence: 1.0,
415
+ tags: ["decision-record"],
416
+ source: "coding-decision",
417
+ // Mirror decisionRecord: persist structuredAttributes.decisionStatus on
418
+ // the replacement so list/get projection and QMD indexing see the
419
+ // authoritative marker (review: supersede omits decisionStatus attrs).
420
+ structuredAttributes: { decisionStatus: "accepted" },
421
+ },
422
+ );
423
+ // Mark the old record superseded: set BOTH frontmatter.status (so
424
+ // recall/search/maintenance exclude it from the active corpus — review P2)
425
+ // AND structuredAttributes.decisionStatus (the decision-specific lifecycle
426
+ // marker used by list/get projection). The content body is not mutated.
427
+ // Rule 25: the replacement is written BEFORE the old record is mutated so
428
+ // a frontmatter-write failure leaves a harmless duplicate, not a missing
429
+ // record. Best-effort: log the failure but don't roll back the replacement
430
+ // (review: cursor partial-write thread).
431
+ try {
432
+ await storage.writeMemoryFrontmatter(oldMemory, {
433
+ status: "archived",
434
+ // Refresh the updated timestamp so the archive/supersede lifecycle event
435
+ // and browse/maintenance sort key reflect when the decision was retired,
436
+ // not when it was originally recorded (review: set updated timestamp when
437
+ // retiring old decisions).
438
+ updated: new Date().toISOString(),
439
+ structuredAttributes: {
440
+ ...(oldMemory.frontmatter.structuredAttributes ?? {}),
441
+ decisionStatus: "superseded",
442
+ },
443
+ });
444
+ } catch (err) {
445
+ log.warn(
446
+ `coding_decision/supersede: replacement ${replacementId} written but old record ${targetId} status update failed — old record will still appear until retried: ${err instanceof Error ? err.message : String(err)}`,
447
+ );
448
+ }
449
+ log.info(
450
+ `access-write op=coding_decision/supersede superseded=${targetId} replacement=${replacementId}`,
451
+ );
452
+ return {
453
+ subcommand: "supersede",
454
+ supersededMemoryId: targetId,
455
+ replacementMemoryId: replacementId,
456
+ };
457
+ }
458
+
459
+ // ──────────────────────────────────────────────────────────────────────────
460
+ // Local helpers
461
+ // ──────────────────────────────────────────────────────────────────────────
462
+
463
+ function safeParseDecisionRecord(content: string): DecisionRecord | null {
464
+ try {
465
+ return parseDecisionRecord(content);
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+
471
+ function raiseInvalidStatus(value: string, ctx: DecisionSurfaceContext): never {
472
+ ctx.throwInputError(
473
+ `invalid decision status "${value}". Valid options: proposed, accepted, superseded, rejected`,
474
+ );
475
+ }