@remnic/core 9.3.688 → 9.3.690

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 (163) hide show
  1. package/dist/access-boundary.d.ts +5 -4
  2. package/dist/access-boundary.js +12 -11
  3. package/dist/access-cli.js +90 -26
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +4 -3
  6. package/dist/access-http.js +15 -14
  7. package/dist/access-mcp.d.ts +11 -3
  8. package/dist/access-mcp.js +14 -13
  9. package/dist/access-operations.d.ts +11 -5
  10. package/dist/access-operations.js +15 -12
  11. package/dist/{access-service-DmCHJ4cH.d.ts → access-service-Dujr3MTm.d.ts} +62 -2
  12. package/dist/access-service.d.ts +4 -3
  13. package/dist/access-service.js +11 -10
  14. package/dist/access-surface-catalog.d.ts +4 -3
  15. package/dist/access-surface-catalog.js +2 -0
  16. package/dist/access-surface-catalog.js.map +1 -1
  17. package/dist/bootstrap.d.ts +3 -2
  18. package/dist/briefing.d.ts +1 -0
  19. package/dist/briefing.js +3 -2
  20. package/dist/buffer.d.ts +1 -0
  21. package/dist/{catalog-COqWZlZ6.d.ts → catalog-CKPtJ114.d.ts} +1 -1
  22. package/dist/causal-consolidation.js +4 -3
  23. package/dist/causal-consolidation.js.map +1 -1
  24. package/dist/{chunk-B4XVLHJA.js → chunk-2N6UNJSX.js} +2 -2
  25. package/dist/{chunk-NN7QYW5W.js → chunk-46URPRE6.js} +2 -2
  26. package/dist/chunk-46URPRE6.js.map +1 -0
  27. package/dist/{chunk-4N3TFFPH.js → chunk-54PVJDO5.js} +2 -2
  28. package/dist/{chunk-PH3HOKYW.js → chunk-67MMWC74.js} +2 -2
  29. package/dist/{chunk-STOEE37X.js → chunk-BKAMHZYR.js} +2 -2
  30. package/dist/{chunk-B43NZNMG.js → chunk-CE6CBRCV.js} +432 -2240
  31. package/dist/chunk-CE6CBRCV.js.map +1 -0
  32. package/dist/{chunk-RVYD6LR3.js → chunk-CP2NZQLT.js} +15 -4
  33. package/dist/chunk-CP2NZQLT.js.map +1 -0
  34. package/dist/{chunk-BLIWOONZ.js → chunk-CUNTLEJP.js} +4 -12
  35. package/dist/chunk-CUNTLEJP.js.map +1 -0
  36. package/dist/{chunk-2SJCWLQD.js → chunk-CWE74HRG.js} +3 -3
  37. package/dist/{chunk-VX6OBUDW.js → chunk-GR77Z2BM.js} +2 -2
  38. package/dist/{chunk-CTOQEZSN.js → chunk-LXIEXSHU.js} +2 -2
  39. package/dist/{chunk-KFBOZYME.js → chunk-NSVXK7M5.js} +33 -4
  40. package/dist/chunk-NSVXK7M5.js.map +1 -0
  41. package/dist/{chunk-473JIN2U.js → chunk-OMKUJTVJ.js} +37 -5
  42. package/dist/chunk-OMKUJTVJ.js.map +1 -0
  43. package/dist/{chunk-GS55WYRL.js → chunk-PHZKALOE.js} +2 -2
  44. package/dist/{chunk-6O6A6YUO.js → chunk-RGNEARXW.js} +3 -3
  45. package/dist/{chunk-4FE2K57M.js → chunk-RTFAZOIR.js} +2 -2
  46. package/dist/{chunk-UTYBJR7M.js → chunk-SB6CQTKP.js} +2 -2
  47. package/dist/chunk-SVOZFLIQ.js +16 -0
  48. package/dist/chunk-SVOZFLIQ.js.map +1 -0
  49. package/dist/{chunk-2KAYTPPT.js → chunk-TYF3D4MS.js} +5 -5
  50. package/dist/{chunk-WIHPNY65.js → chunk-UD5OKH4J.js} +2 -2
  51. package/dist/{chunk-QANVLERJ.js → chunk-UPIBE2DK.js} +2 -2
  52. package/dist/{chunk-CCWHPGT4.js → chunk-WROKD3XC.js} +45 -30
  53. package/dist/chunk-WROKD3XC.js.map +1 -0
  54. package/dist/{chunk-ZPTISBQU.js → chunk-X5J3JZR3.js} +7 -7
  55. package/dist/{chunk-OV4D5T7V.js → chunk-X7RLU5CR.js} +2 -2
  56. package/dist/{chunk-S2OU5DZY.js → chunk-XKUKJIOY.js} +30 -7
  57. package/dist/chunk-XKUKJIOY.js.map +1 -0
  58. package/dist/{chunk-FUCUR2OZ.js → chunk-Z7KILAOU.js} +603 -9
  59. package/dist/chunk-Z7KILAOU.js.map +1 -0
  60. package/dist/chunk-ZU7N3S6V.js +2190 -0
  61. package/dist/chunk-ZU7N3S6V.js.map +1 -0
  62. package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
  63. package/dist/{cli-D8nZ2MPH.d.ts → cli-BkDp6WNi.d.ts} +2 -2
  64. package/dist/cli.d.ts +5 -4
  65. package/dist/cli.js +32 -30
  66. package/dist/compounding/engine.d.ts +1 -0
  67. package/dist/compounding/engine.js +3 -2
  68. package/dist/connectors/codex-materialize-runner.js +3 -2
  69. package/dist/connectors/index.js +3 -2
  70. package/dist/consolidation-provenance-check.d.ts +1 -0
  71. package/dist/consolidation-undo.d.ts +1 -0
  72. package/dist/contradiction/index.d.ts +1 -0
  73. package/dist/conversation-index/backend.js +2 -2
  74. package/dist/entity-retrieval.d.ts +1 -0
  75. package/dist/entity-retrieval.js +3 -2
  76. package/dist/explicit-capture.d.ts +3 -2
  77. package/dist/index.d.ts +6 -5
  78. package/dist/index.js +47 -44
  79. package/dist/index.js.map +1 -1
  80. package/dist/lcm/engine.js +3 -3
  81. package/dist/lcm/index.js +5 -5
  82. package/dist/maintenance/memory-governance.js +3 -2
  83. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -2
  84. package/dist/maintenance/rebuild-memory-projection.js +4 -3
  85. package/dist/mcp-memory-inspector-app.d.ts +4 -3
  86. package/dist/memory-worth-outcomes.d.ts +1 -0
  87. package/dist/namespaces/migrate.d.ts +2 -1
  88. package/dist/namespaces/migrate.js +10 -9
  89. package/dist/namespaces/search.js +6 -6
  90. package/dist/namespaces/storage.d.ts +2 -1
  91. package/dist/namespaces/storage.js +3 -2
  92. package/dist/operator-toolkit.d.ts +1 -0
  93. package/dist/operator-toolkit.js +13 -11
  94. package/dist/{orchestrator-CA6ouzBn.d.ts → orchestrator-B7ixmUkP.d.ts} +145 -1
  95. package/dist/orchestrator.d.ts +3 -2
  96. package/dist/orchestrator.js +22 -20
  97. package/dist/recall-pipeline-stages.d.ts +167 -0
  98. package/dist/recall-pipeline-stages.js +61 -0
  99. package/dist/recall-pipeline-stages.js.map +1 -0
  100. package/dist/schemas.d.ts +10 -10
  101. package/dist/search/factory.js +5 -5
  102. package/dist/search/index.js +9 -9
  103. package/dist/semantic-consolidation.js +4 -3
  104. package/dist/semantic-rule-promotion.js +3 -2
  105. package/dist/semantic-rule-verifier.js +3 -2
  106. package/dist/storage.d.ts +3 -27
  107. package/dist/storage.js +5 -3
  108. package/dist/structured-attributes.d.ts +29 -0
  109. package/dist/structured-attributes.js +8 -0
  110. package/dist/structured-attributes.js.map +1 -0
  111. package/dist/temporal-supersession.d.ts +1 -0
  112. package/dist/tier-migration.d.ts +1 -0
  113. package/dist/verified-recall.js +3 -2
  114. package/package.json +2 -2
  115. package/src/access-boundary.ts +2 -1
  116. package/src/access-cli.test.ts +40 -0
  117. package/src/access-cli.ts +90 -2
  118. package/src/access-http.ts +35 -6
  119. package/src/access-mcp.ts +34 -0
  120. package/src/access-operations.ts +45 -0
  121. package/src/access-service.ts +60 -0
  122. package/src/access-surface-catalog.test.ts +1 -1
  123. package/src/access-surface-catalog.ts +2 -0
  124. package/src/cli.ts +18 -0
  125. package/src/coding/architecture-card.test.ts +544 -0
  126. package/src/coding/architecture-card.ts +687 -0
  127. package/src/coding/architecture-surfaces.test.ts +579 -0
  128. package/src/coding/architecture-surfaces.ts +457 -0
  129. package/src/maintenance/namespace-maintenance-fanout.test.ts +595 -0
  130. package/src/maintenance/namespace-maintenance-fanout.ts +318 -0
  131. package/src/maintenance/namespace-planner.ts +74 -16
  132. package/src/operator-toolkit.ts +25 -0
  133. package/src/orchestrator.ts +144 -0
  134. package/src/recall-pipeline-stages.test.ts +269 -0
  135. package/src/recall-pipeline-stages.ts +294 -0
  136. package/src/storage.ts +6 -20
  137. package/src/structured-attributes.ts +39 -0
  138. package/dist/chunk-473JIN2U.js.map +0 -1
  139. package/dist/chunk-B43NZNMG.js.map +0 -1
  140. package/dist/chunk-BLIWOONZ.js.map +0 -1
  141. package/dist/chunk-CCWHPGT4.js.map +0 -1
  142. package/dist/chunk-FUCUR2OZ.js.map +0 -1
  143. package/dist/chunk-KFBOZYME.js.map +0 -1
  144. package/dist/chunk-NN7QYW5W.js.map +0 -1
  145. package/dist/chunk-RVYD6LR3.js.map +0 -1
  146. package/dist/chunk-S2OU5DZY.js.map +0 -1
  147. /package/dist/{chunk-B4XVLHJA.js.map → chunk-2N6UNJSX.js.map} +0 -0
  148. /package/dist/{chunk-4N3TFFPH.js.map → chunk-54PVJDO5.js.map} +0 -0
  149. /package/dist/{chunk-PH3HOKYW.js.map → chunk-67MMWC74.js.map} +0 -0
  150. /package/dist/{chunk-STOEE37X.js.map → chunk-BKAMHZYR.js.map} +0 -0
  151. /package/dist/{chunk-2SJCWLQD.js.map → chunk-CWE74HRG.js.map} +0 -0
  152. /package/dist/{chunk-VX6OBUDW.js.map → chunk-GR77Z2BM.js.map} +0 -0
  153. /package/dist/{chunk-CTOQEZSN.js.map → chunk-LXIEXSHU.js.map} +0 -0
  154. /package/dist/{chunk-GS55WYRL.js.map → chunk-PHZKALOE.js.map} +0 -0
  155. /package/dist/{chunk-6O6A6YUO.js.map → chunk-RGNEARXW.js.map} +0 -0
  156. /package/dist/{chunk-4FE2K57M.js.map → chunk-RTFAZOIR.js.map} +0 -0
  157. /package/dist/{chunk-UTYBJR7M.js.map → chunk-SB6CQTKP.js.map} +0 -0
  158. /package/dist/{chunk-2KAYTPPT.js.map → chunk-TYF3D4MS.js.map} +0 -0
  159. /package/dist/{chunk-WIHPNY65.js.map → chunk-UD5OKH4J.js.map} +0 -0
  160. /package/dist/{chunk-QANVLERJ.js.map → chunk-UPIBE2DK.js.map} +0 -0
  161. /package/dist/{chunk-ZPTISBQU.js.map → chunk-X5J3JZR3.js.map} +0 -0
  162. /package/dist/{chunk-OV4D5T7V.js.map → chunk-X7RLU5CR.js.map} +0 -0
  163. /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Architecture-card surface contract + handler (issue #1548 Track A PR 3).
3
+ *
4
+ * Mirrors the decision-surfaces pattern (PR 2): one shared implementation
5
+ * behind the MCP, HTTP, and CLI surfaces. All three transports dispatch
6
+ * through the `coding_architecture` boundary operation, which calls
7
+ * {@link handleCodingArchitecture} via the service delegate.
8
+ *
9
+ * Gate (rule 39): `codingKnowledge.enabled + architectureCard + coding
10
+ * context` — one predicate, identical on every surface. The tools/list
11
+ * visibility gate checks conditions 1–2 only (config-level).
12
+ *
13
+ * Persistence (rule 43): the card is written through the storage
14
+ * manager's normal persist pipeline (writeMemory for the initial card,
15
+ * updateMemory for refreshes) so catalog recording + reindex fire. No
16
+ * direct `fs` writes of memory content. Each refresh snapshots the
17
+ * prior content via page-versioning before overwriting (rule 25 —
18
+ * preserve the old before the new is ready).
19
+ *
20
+ * Subcommands:
21
+ * - `get`: return the current architecture card (or not-found).
22
+ * - `refresh`: build a fresh card from the repo, persist it, and
23
+ * snapshot the prior version.
24
+ */
25
+ import type {
26
+ CodingKnowledgeConfig,
27
+ CodingContext,
28
+ MemoryFile,
29
+ MemoryFrontmatter,
30
+ MemoryStatus,
31
+ } from "../types.js";
32
+ import type { VersionTrigger, VersioningConfig } from "../page-versioning.js";
33
+ import { stripAttributesSuffix } from "../structured-attributes.js";
34
+ import { ARCHITECTURE_CARD_TRUNCATION_MARKER, type ArchitectureCardBuildResult } from "./architecture-card.js";
35
+ import { log } from "../logger.js";
36
+
37
+ // ──────────────────────────────────────────────────────────────────────────
38
+ // Subcommands
39
+ // ──────────────────────────────────────────────────────────────────────────
40
+
41
+ export const ARCHITECTURE_SUBCOMMANDS = ["get", "refresh"] as const;
42
+
43
+ export type ArchitectureSubcommand = (typeof ARCHITECTURE_SUBCOMMANDS)[number];
44
+
45
+ const SUBCOMMAND_VALUES = ARCHITECTURE_SUBCOMMANDS as readonly string[];
46
+
47
+ /** Type guard — narrows an unknown subcommand string. */
48
+ export function isArchitectureSubcommand(value: unknown): value is ArchitectureSubcommand {
49
+ return typeof value === "string" && SUBCOMMAND_VALUES.includes(value);
50
+ }
51
+
52
+ /** Human-readable subcommand list for error messages (rule 51). */
53
+ export function formatArchitectureSubcommands(): string {
54
+ return ARCHITECTURE_SUBCOMMANDS.join(", ");
55
+ }
56
+
57
+ // ──────────────────────────────────────────────────────────────────────────
58
+ // Gate predicates — rule 39: one predicate, identical on every surface
59
+ // ──────────────────────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * The single architecture-card surface gate. Returns `true` only when:
63
+ * 1. `codingKnowledge.enabled` is true,
64
+ * 2. `codingKnowledge.architectureCard` is true,
65
+ * 3. a coding context is attached (so a repo root is available).
66
+ */
67
+ export function isArchitectureCardSurfaceEnabled(
68
+ config: CodingKnowledgeConfig,
69
+ codingContext: CodingContext | null | undefined,
70
+ ): boolean {
71
+ return (
72
+ config.enabled === true &&
73
+ config.architectureCard === true &&
74
+ codingContext !== null &&
75
+ codingContext !== undefined
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Config-only visibility gate — used by the MCP constructor to decide
81
+ * whether to advertise `engram.coding_architecture` in `tools/list`.
82
+ * When this returns `false` the tools array is byte-identical to
83
+ * pre-feature (rule 39).
84
+ */
85
+ export function isArchitectureCardSurfaceVisible(
86
+ config: CodingKnowledgeConfig,
87
+ ): boolean {
88
+ return config.enabled === true && config.architectureCard === true;
89
+ }
90
+
91
+ // ──────────────────────────────────────────────────────────────────────────
92
+ // Surface request / response shapes
93
+ // ──────────────────────────────────────────────────────────────────────────
94
+
95
+ /** Canonical surface request — one shape for all three transports. */
96
+ export interface ArchitectureSurfaceRequest {
97
+ subcommand: ArchitectureSubcommand;
98
+ sessionKey?: string;
99
+ namespace?: string;
100
+ }
101
+
102
+ /** Surface response — discriminated union on `subcommand`. */
103
+ export type ArchitectureSurfaceResponse =
104
+ | {
105
+ subcommand: "get";
106
+ found: boolean;
107
+ card?: {
108
+ content: string;
109
+ generatedAt: string;
110
+ byteSize: number;
111
+ truncated: boolean;
112
+ };
113
+ }
114
+ | {
115
+ subcommand: "refresh";
116
+ refreshed: boolean;
117
+ memoryId: string;
118
+ byteSize: number;
119
+ truncated: boolean;
120
+ buildCode?: string;
121
+ };
122
+
123
+ // ──────────────────────────────────────────────────────────────────────────
124
+ // Storage contract — narrow subset of StorageManager
125
+ // ──────────────────────────────────────────────────────────────────────────
126
+
127
+ /** Tag identifying the architecture card memory (single card per namespace). */
128
+ export const ARCHITECTURE_CARD_TAG = "architecture-card";
129
+
130
+ /** Structured attribute marking a memory as the architecture card. */
131
+ export const ARCHITECTURE_CARD_KIND = "architecture";
132
+
133
+ /**
134
+ * Structural subset of StorageManager the architecture handler reads or
135
+ * writes. Kept narrow so the module stays decoupled from storage.ts and
136
+ * is unit-testable with a stub.
137
+ */
138
+ export interface ArchitectureSurfaceStorage {
139
+ readonly dir: string;
140
+ /** The resolved namespace — used for logging/observability. */
141
+ readonly namespace: string;
142
+ readAllMemories(): Promise<readonly MemoryFile[]>;
143
+ writeMemory(
144
+ category: "fact",
145
+ content: string,
146
+ options: {
147
+ confidence?: number;
148
+ tags?: string[];
149
+ source?: string;
150
+ status?: MemoryStatus;
151
+ structuredAttributes?: Record<string, string>;
152
+ },
153
+ ): Promise<string>;
154
+ updateMemory(
155
+ id: string,
156
+ newContent: string,
157
+ options?: { supersedes?: string; lineage?: string[]; actor?: string },
158
+ ): Promise<boolean>;
159
+ }
160
+
161
+ /**
162
+ * Card-builder dependency. The handler delegates repo scanning to this
163
+ * function so tests can inject a fixed card without touching the
164
+ * filesystem.
165
+ */
166
+ export type ArchitectureCardBuilder = (
167
+ repoRoot: string,
168
+ ) => Promise<ArchitectureCardBuildResult>;
169
+
170
+ /**
171
+ * Page-versioning snapshot dependency. The handler calls this BEFORE
172
+ * overwriting the card content so the prior version is preserved
173
+ * (rule 25).
174
+ */
175
+ export interface ArchitectureVersioningHook {
176
+ /**
177
+ * Snapshot the current content of the card memory before refresh.
178
+ * Failures are logged but do not block the refresh (best-effort).
179
+ */
180
+ snapshotIfExists(memory: MemoryFile): Promise<void>;
181
+ }
182
+
183
+ /**
184
+ * Dependencies the handler borrows from the service. The service
185
+ * constructs this context per call; the handler never touches the
186
+ * orchestrator directly.
187
+ */
188
+ export interface ArchitectureSurfaceContext {
189
+ readonly codingKnowledge: CodingKnowledgeConfig;
190
+ getCodingContext(sessionKey: string): CodingContext | null;
191
+ /** Resolve storage through the SAME namespace path as memory_store. */
192
+ resolveStorage(request: ArchitectureSurfaceRequest): Promise<ArchitectureSurfaceStorage>;
193
+ /** Build the architecture card from a repo root. */
194
+ buildCard: ArchitectureCardBuilder;
195
+ /** Snapshot the prior card version before refresh (rule 25). */
196
+ versioning: ArchitectureVersioningHook;
197
+ /** Throw the surface-appropriate input-validation error. */
198
+ throwInputError(message: string): never;
199
+ }
200
+
201
+ // ──────────────────────────────────────────────────────────────────────────
202
+ // Handler — the single shared implementation behind all three surfaces
203
+ // ──────────────────────────────────────────────────────────────────────────
204
+
205
+ /**
206
+ * The single shared implementation behind the MCP, HTTP, and CLI
207
+ * architecture-card surfaces.
208
+ *
209
+ * Gate (rule 39): `codingKnowledge.enabled + architectureCard + coding
210
+ * context`. Persistence (rule 43): cards are written through the storage
211
+ * manager's normal persist pipeline. Refresh (rule 25): the prior card
212
+ * is snapshotted via page-versioning BEFORE the content is overwritten.
213
+ */
214
+ export async function handleCodingArchitecture(
215
+ request: ArchitectureSurfaceRequest,
216
+ ctx: ArchitectureSurfaceContext,
217
+ ): Promise<ArchitectureSurfaceResponse> {
218
+ const codingContext = request.sessionKey
219
+ ? ctx.getCodingContext(request.sessionKey)
220
+ : null;
221
+ // Rule 39 + type-guard narrowing: check `codingContext` inline so TS
222
+ // narrows it to CodingContext here, with no non-null assertion cast.
223
+ if (
224
+ codingContext === null ||
225
+ !isArchitectureCardSurfaceEnabled(ctx.codingKnowledge, codingContext)
226
+ ) {
227
+ ctx.throwInputError(
228
+ "coding_architecture requires codingKnowledge.enabled, codingKnowledge.architectureCard, and an attached coding context",
229
+ );
230
+ }
231
+ switch (request.subcommand) {
232
+ case "get":
233
+ return architectureGet(request, ctx);
234
+ case "refresh":
235
+ return architectureRefresh(request, ctx, codingContext);
236
+ }
237
+ }
238
+
239
+ // ──────────────────────────────────────────────────────────────────────────
240
+ // Subcommand handlers
241
+ // ──────────────────────────────────────────────────────────────────────────
242
+
243
+
244
+ async function architectureGet(
245
+ request: ArchitectureSurfaceRequest,
246
+ ctx: ArchitectureSurfaceContext,
247
+ ): Promise<ArchitectureSurfaceResponse> {
248
+ const storage = await ctx.resolveStorage(request);
249
+ const existing = await findArchitectureCardMemory(storage);
250
+ if (!existing) {
251
+ return { subcommand: "get", found: false };
252
+ }
253
+ const content = stripAttributesSuffix(existing.content);
254
+ return {
255
+ subcommand: "get",
256
+ found: true,
257
+ card: {
258
+ content,
259
+ generatedAt: existing.frontmatter.updated ?? existing.frontmatter.created,
260
+ byteSize: Buffer.byteLength(content, "utf-8"),
261
+ // Derive truncation from the CONTENT marker, not a frontmatter tag —
262
+ // the refresh update path changes content without rewriting tags, so
263
+ // a tag-based check goes stale (cursor review: stale truncated tag).
264
+ truncated: content.includes(ARCHITECTURE_CARD_TRUNCATION_MARKER),
265
+ },
266
+ };
267
+ }
268
+
269
+ async function architectureRefresh(
270
+ request: ArchitectureSurfaceRequest,
271
+ ctx: ArchitectureSurfaceContext,
272
+ codingContext: CodingContext,
273
+ ): Promise<ArchitectureSurfaceResponse> {
274
+ const repoRoot = codingContext.rootPath ?? codingContext.projectId;
275
+ // Authorize the write + resolve storage BEFORE scanning the repo or
276
+ // invoking the summariser, so an unauthorized refresh fails fast instead
277
+ // of doing work (and an LLM call) it cannot persist (codex review: buildCard
278
+ // ran before the write ACL was enforced).
279
+ const storage = await ctx.resolveStorage(request);
280
+ const buildResult = await ctx.buildCard(repoRoot);
281
+ if (!buildResult.ok) {
282
+ // A failed build is a tagged outcome, not a crash. Log the full detail
283
+ // for operators; it may include absolute paths / raw I/O messages that
284
+ // must NOT reach clients (cursor review: raw build errors reach clients).
285
+ log.warn(
286
+ `coding_architecture/refresh: card build failed (code=${buildResult.code}): ${buildResult.detail}`,
287
+ );
288
+ // Persist nothing on build failure — no stale card left behind (rule 44).
289
+ // Client-facing error carries only the code.
290
+ ctx.throwInputError(
291
+ `coding_architecture refresh failed: card build returned ${buildResult.code}`,
292
+ );
293
+ }
294
+
295
+ const cardContent = buildResult.card.content;
296
+ const tags = [ARCHITECTURE_CARD_TAG];
297
+ if (buildResult.card.truncated) tags.push("truncated");
298
+
299
+ const existing = await findArchitectureCardMemory(storage);
300
+ if (existing) {
301
+ // rule 25: snapshot the prior content BEFORE overwriting.
302
+ await ctx.versioning.snapshotIfExists(existing);
303
+ const updated = await storage.updateMemory(existing.frontmatter.id, cardContent, {
304
+ actor: "coding-architecture-refresh",
305
+ });
306
+ if (!updated) {
307
+ log.warn(
308
+ `coding_architecture/refresh: updateMemory returned false for id=${existing.frontmatter.id} — falling back to writeMemory`,
309
+ );
310
+ const memoryId = await storage.writeMemory("fact", cardContent, {
311
+ confidence: 1.0,
312
+ tags,
313
+ source: "coding-architecture",
314
+ structuredAttributes: { cardKind: ARCHITECTURE_CARD_KIND },
315
+ });
316
+ log.info(
317
+ `access-write op=coding_architecture/refresh memoryId=${memoryId} (fallback write) byteSize=${buildResult.card.byteSize}`,
318
+ );
319
+ return {
320
+ subcommand: "refresh",
321
+ refreshed: true,
322
+ memoryId,
323
+ byteSize: buildResult.card.byteSize,
324
+ truncated: buildResult.card.truncated,
325
+ };
326
+ }
327
+ log.info(
328
+ `access-write op=coding_architecture/refresh memoryId=${existing.frontmatter.id} (updated) byteSize=${buildResult.card.byteSize}`,
329
+ );
330
+ return {
331
+ subcommand: "refresh",
332
+ refreshed: true,
333
+ memoryId: existing.frontmatter.id,
334
+ byteSize: buildResult.card.byteSize,
335
+ truncated: buildResult.card.truncated,
336
+ };
337
+ }
338
+
339
+ // First card for this namespace — write a new memory.
340
+ const memoryId = await storage.writeMemory("fact", cardContent, {
341
+ confidence: 1.0,
342
+ tags,
343
+ source: "coding-architecture",
344
+ structuredAttributes: { cardKind: ARCHITECTURE_CARD_KIND },
345
+ });
346
+ log.info(
347
+ `access-write op=coding_architecture/refresh memoryId=${memoryId} (new) byteSize=${buildResult.card.byteSize}`,
348
+ );
349
+ return {
350
+ subcommand: "refresh",
351
+ refreshed: true,
352
+ memoryId,
353
+ byteSize: buildResult.card.byteSize,
354
+ truncated: buildResult.card.truncated,
355
+ };
356
+ }
357
+
358
+ // ──────────────────────────────────────────────────────────────────────────
359
+ // Local helpers
360
+ // ──────────────────────────────────────────────────────────────────────────
361
+
362
+ /**
363
+ * Find the existing architecture-card memory in the namespace, if any.
364
+ * Identified by the `architecture-card` tag + `cardKind=architecture`
365
+ * structured attribute. A single card per namespace is the invariant;
366
+ * if multiple exist (race), the most recently updated wins.
367
+ */
368
+ export async function findArchitectureCardMemory(
369
+ storage: ArchitectureSurfaceStorage,
370
+ ): Promise<MemoryFile | null> {
371
+ const memories = await storage.readAllMemories();
372
+ const cards = memories.filter((m) => {
373
+ if (m.frontmatter.category !== "fact") return false;
374
+ const tags = m.frontmatter.tags ?? [];
375
+ if (!tags.includes(ARCHITECTURE_CARD_TAG)) return false;
376
+ // Require the cardKind structured attribute so a user-created fact that
377
+ // merely happens to be tagged "architecture-card" is NOT mistaken for
378
+ // the managed card and overwritten on refresh (codex review).
379
+ if (m.frontmatter.structuredAttributes?.cardKind !== ARCHITECTURE_CARD_KIND) return false;
380
+ // Any outer status other than undefined/"active" means retired.
381
+ const outer = m.frontmatter.status;
382
+ if (outer !== undefined && outer !== "active") return false;
383
+ // Also skip cards archived via archivedAt without an explicit status
384
+ // (cursor review: archived cards can still match the tag filter).
385
+ if (m.frontmatter.archivedAt !== undefined && m.frontmatter.archivedAt !== null) return false;
386
+ return true;
387
+ });
388
+ if (cards.length === 0) return null;
389
+ // Most recently updated wins (deterministic tie-break by id).
390
+ cards.sort((a, b) => {
391
+ const aUpdated = a.frontmatter.updated ?? a.frontmatter.created;
392
+ const bUpdated = b.frontmatter.updated ?? b.frontmatter.created;
393
+ if (aUpdated !== bUpdated) return bUpdated.localeCompare(aUpdated);
394
+ return b.frontmatter.id.localeCompare(a.frontmatter.id);
395
+ });
396
+ return cards[0] ?? null;
397
+ }
398
+
399
+ /**
400
+ * Build a {@link MemoryFrontmatter}-shaped patch that marks a memory as
401
+ * the architecture card. Exported for test fixtures.
402
+ */
403
+ export function architectureCardFrontmatterMarker(): Partial<MemoryFrontmatter> {
404
+ return {
405
+ tags: [ARCHITECTURE_CARD_TAG],
406
+ };
407
+ }
408
+
409
+ /** Page-version snapshot writer (decoupled type — matches page-versioning.createVersion's call). */
410
+ type CreatePageVersion = (
411
+ pagePath: string,
412
+ content: string,
413
+ trigger: VersionTrigger,
414
+ config: VersioningConfig,
415
+ logger: undefined,
416
+ note: string,
417
+ memoryDir: string,
418
+ ) => Promise<unknown>;
419
+
420
+ /**
421
+ * Build the page-versioning snapshot hook from operator config.
422
+ *
423
+ * - Skips entirely when versioning is disabled — no sidecars written for a
424
+ * feature the operator turned off (codex review: hardcoded config ignored
425
+ * operator settings).
426
+ * - Reads the FULL file (frontmatter + body) from disk so a revert restores
427
+ * the complete memory, not just the body (codex review: snapshot was
428
+ * body-only).
429
+ */
430
+ export function createArchitectureVersioningHook(
431
+ enabled: boolean,
432
+ maxVersionsPerPage: number,
433
+ sidecarDir: string,
434
+ memoryDir: string,
435
+ readFile: (path: string) => Promise<string>,
436
+ writeVersion: CreatePageVersion,
437
+ ): ArchitectureVersioningHook {
438
+ return {
439
+ async snapshotIfExists(memory) {
440
+ if (!enabled) return;
441
+ try {
442
+ const fullContent = await readFile(memory.path);
443
+ await writeVersion(
444
+ memory.path,
445
+ fullContent,
446
+ "manual",
447
+ { enabled: true, maxVersionsPerPage, sidecarDir },
448
+ undefined,
449
+ "architecture-card-refresh",
450
+ memoryDir,
451
+ );
452
+ } catch {
453
+ // Best-effort — a snapshot failure does not block the refresh.
454
+ }
455
+ },
456
+ };
457
+ }