@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.
- package/dist/access-boundary.d.ts +5 -4
- package/dist/access-boundary.js +12 -11
- package/dist/access-cli.js +90 -26
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +4 -3
- package/dist/access-http.js +15 -14
- package/dist/access-mcp.d.ts +11 -3
- package/dist/access-mcp.js +14 -13
- package/dist/access-operations.d.ts +11 -5
- package/dist/access-operations.js +15 -12
- package/dist/{access-service-DmCHJ4cH.d.ts → access-service-Dujr3MTm.d.ts} +62 -2
- package/dist/access-service.d.ts +4 -3
- package/dist/access-service.js +11 -10
- package/dist/access-surface-catalog.d.ts +4 -3
- package/dist/access-surface-catalog.js +2 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/bootstrap.d.ts +3 -2
- package/dist/briefing.d.ts +1 -0
- package/dist/briefing.js +3 -2
- package/dist/buffer.d.ts +1 -0
- package/dist/{catalog-COqWZlZ6.d.ts → catalog-CKPtJ114.d.ts} +1 -1
- package/dist/causal-consolidation.js +4 -3
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-B4XVLHJA.js → chunk-2N6UNJSX.js} +2 -2
- package/dist/{chunk-NN7QYW5W.js → chunk-46URPRE6.js} +2 -2
- package/dist/chunk-46URPRE6.js.map +1 -0
- package/dist/{chunk-4N3TFFPH.js → chunk-54PVJDO5.js} +2 -2
- package/dist/{chunk-PH3HOKYW.js → chunk-67MMWC74.js} +2 -2
- package/dist/{chunk-STOEE37X.js → chunk-BKAMHZYR.js} +2 -2
- package/dist/{chunk-B43NZNMG.js → chunk-CE6CBRCV.js} +432 -2240
- package/dist/chunk-CE6CBRCV.js.map +1 -0
- package/dist/{chunk-RVYD6LR3.js → chunk-CP2NZQLT.js} +15 -4
- package/dist/chunk-CP2NZQLT.js.map +1 -0
- package/dist/{chunk-BLIWOONZ.js → chunk-CUNTLEJP.js} +4 -12
- package/dist/chunk-CUNTLEJP.js.map +1 -0
- package/dist/{chunk-2SJCWLQD.js → chunk-CWE74HRG.js} +3 -3
- package/dist/{chunk-VX6OBUDW.js → chunk-GR77Z2BM.js} +2 -2
- package/dist/{chunk-CTOQEZSN.js → chunk-LXIEXSHU.js} +2 -2
- package/dist/{chunk-KFBOZYME.js → chunk-NSVXK7M5.js} +33 -4
- package/dist/chunk-NSVXK7M5.js.map +1 -0
- package/dist/{chunk-473JIN2U.js → chunk-OMKUJTVJ.js} +37 -5
- package/dist/chunk-OMKUJTVJ.js.map +1 -0
- package/dist/{chunk-GS55WYRL.js → chunk-PHZKALOE.js} +2 -2
- package/dist/{chunk-6O6A6YUO.js → chunk-RGNEARXW.js} +3 -3
- package/dist/{chunk-4FE2K57M.js → chunk-RTFAZOIR.js} +2 -2
- package/dist/{chunk-UTYBJR7M.js → chunk-SB6CQTKP.js} +2 -2
- package/dist/chunk-SVOZFLIQ.js +16 -0
- package/dist/chunk-SVOZFLIQ.js.map +1 -0
- package/dist/{chunk-2KAYTPPT.js → chunk-TYF3D4MS.js} +5 -5
- package/dist/{chunk-WIHPNY65.js → chunk-UD5OKH4J.js} +2 -2
- package/dist/{chunk-QANVLERJ.js → chunk-UPIBE2DK.js} +2 -2
- package/dist/{chunk-CCWHPGT4.js → chunk-WROKD3XC.js} +45 -30
- package/dist/chunk-WROKD3XC.js.map +1 -0
- package/dist/{chunk-ZPTISBQU.js → chunk-X5J3JZR3.js} +7 -7
- package/dist/{chunk-OV4D5T7V.js → chunk-X7RLU5CR.js} +2 -2
- package/dist/{chunk-S2OU5DZY.js → chunk-XKUKJIOY.js} +30 -7
- package/dist/chunk-XKUKJIOY.js.map +1 -0
- package/dist/{chunk-FUCUR2OZ.js → chunk-Z7KILAOU.js} +603 -9
- package/dist/chunk-Z7KILAOU.js.map +1 -0
- package/dist/chunk-ZU7N3S6V.js +2190 -0
- package/dist/chunk-ZU7N3S6V.js.map +1 -0
- package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
- package/dist/{cli-D8nZ2MPH.d.ts → cli-BkDp6WNi.d.ts} +2 -2
- package/dist/cli.d.ts +5 -4
- package/dist/cli.js +32 -30
- package/dist/compounding/engine.d.ts +1 -0
- package/dist/compounding/engine.js +3 -2
- package/dist/connectors/codex-materialize-runner.js +3 -2
- package/dist/connectors/index.js +3 -2
- package/dist/consolidation-provenance-check.d.ts +1 -0
- package/dist/consolidation-undo.d.ts +1 -0
- package/dist/contradiction/index.d.ts +1 -0
- package/dist/conversation-index/backend.js +2 -2
- package/dist/entity-retrieval.d.ts +1 -0
- package/dist/entity-retrieval.js +3 -2
- package/dist/explicit-capture.d.ts +3 -2
- package/dist/index.d.ts +6 -5
- package/dist/index.js +47 -44
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +5 -5
- package/dist/maintenance/memory-governance.js +3 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -2
- package/dist/maintenance/rebuild-memory-projection.js +4 -3
- package/dist/mcp-memory-inspector-app.d.ts +4 -3
- package/dist/memory-worth-outcomes.d.ts +1 -0
- package/dist/namespaces/migrate.d.ts +2 -1
- package/dist/namespaces/migrate.js +10 -9
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.d.ts +2 -1
- package/dist/namespaces/storage.js +3 -2
- package/dist/operator-toolkit.d.ts +1 -0
- package/dist/operator-toolkit.js +13 -11
- package/dist/{orchestrator-CA6ouzBn.d.ts → orchestrator-B7ixmUkP.d.ts} +145 -1
- package/dist/orchestrator.d.ts +3 -2
- package/dist/orchestrator.js +22 -20
- package/dist/recall-pipeline-stages.d.ts +167 -0
- package/dist/recall-pipeline-stages.js +61 -0
- package/dist/recall-pipeline-stages.js.map +1 -0
- package/dist/schemas.d.ts +10 -10
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +4 -3
- package/dist/semantic-rule-promotion.js +3 -2
- package/dist/semantic-rule-verifier.js +3 -2
- package/dist/storage.d.ts +3 -27
- package/dist/storage.js +5 -3
- package/dist/structured-attributes.d.ts +29 -0
- package/dist/structured-attributes.js +8 -0
- package/dist/structured-attributes.js.map +1 -0
- package/dist/temporal-supersession.d.ts +1 -0
- package/dist/tier-migration.d.ts +1 -0
- package/dist/verified-recall.js +3 -2
- package/package.json +2 -2
- package/src/access-boundary.ts +2 -1
- package/src/access-cli.test.ts +40 -0
- package/src/access-cli.ts +90 -2
- package/src/access-http.ts +35 -6
- package/src/access-mcp.ts +34 -0
- package/src/access-operations.ts +45 -0
- package/src/access-service.ts +60 -0
- package/src/access-surface-catalog.test.ts +1 -1
- package/src/access-surface-catalog.ts +2 -0
- package/src/cli.ts +18 -0
- package/src/coding/architecture-card.test.ts +544 -0
- package/src/coding/architecture-card.ts +687 -0
- package/src/coding/architecture-surfaces.test.ts +579 -0
- package/src/coding/architecture-surfaces.ts +457 -0
- package/src/maintenance/namespace-maintenance-fanout.test.ts +595 -0
- package/src/maintenance/namespace-maintenance-fanout.ts +318 -0
- package/src/maintenance/namespace-planner.ts +74 -16
- package/src/operator-toolkit.ts +25 -0
- package/src/orchestrator.ts +144 -0
- package/src/recall-pipeline-stages.test.ts +269 -0
- package/src/recall-pipeline-stages.ts +294 -0
- package/src/storage.ts +6 -20
- package/src/structured-attributes.ts +39 -0
- package/dist/chunk-473JIN2U.js.map +0 -1
- package/dist/chunk-B43NZNMG.js.map +0 -1
- package/dist/chunk-BLIWOONZ.js.map +0 -1
- package/dist/chunk-CCWHPGT4.js.map +0 -1
- package/dist/chunk-FUCUR2OZ.js.map +0 -1
- package/dist/chunk-KFBOZYME.js.map +0 -1
- package/dist/chunk-NN7QYW5W.js.map +0 -1
- package/dist/chunk-RVYD6LR3.js.map +0 -1
- package/dist/chunk-S2OU5DZY.js.map +0 -1
- /package/dist/{chunk-B4XVLHJA.js.map → chunk-2N6UNJSX.js.map} +0 -0
- /package/dist/{chunk-4N3TFFPH.js.map → chunk-54PVJDO5.js.map} +0 -0
- /package/dist/{chunk-PH3HOKYW.js.map → chunk-67MMWC74.js.map} +0 -0
- /package/dist/{chunk-STOEE37X.js.map → chunk-BKAMHZYR.js.map} +0 -0
- /package/dist/{chunk-2SJCWLQD.js.map → chunk-CWE74HRG.js.map} +0 -0
- /package/dist/{chunk-VX6OBUDW.js.map → chunk-GR77Z2BM.js.map} +0 -0
- /package/dist/{chunk-CTOQEZSN.js.map → chunk-LXIEXSHU.js.map} +0 -0
- /package/dist/{chunk-GS55WYRL.js.map → chunk-PHZKALOE.js.map} +0 -0
- /package/dist/{chunk-6O6A6YUO.js.map → chunk-RGNEARXW.js.map} +0 -0
- /package/dist/{chunk-4FE2K57M.js.map → chunk-RTFAZOIR.js.map} +0 -0
- /package/dist/{chunk-UTYBJR7M.js.map → chunk-SB6CQTKP.js.map} +0 -0
- /package/dist/{chunk-2KAYTPPT.js.map → chunk-TYF3D4MS.js.map} +0 -0
- /package/dist/{chunk-WIHPNY65.js.map → chunk-UD5OKH4J.js.map} +0 -0
- /package/dist/{chunk-QANVLERJ.js.map → chunk-UPIBE2DK.js.map} +0 -0
- /package/dist/{chunk-ZPTISBQU.js.map → chunk-X5J3JZR3.js.map} +0 -0
- /package/dist/{chunk-OV4D5T7V.js.map → chunk-X7RLU5CR.js.map} +0 -0
- /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface contract tests for the architecture-card handler
|
|
3
|
+
* (issue #1548 Track A PR 3).
|
|
4
|
+
*
|
|
5
|
+
* Contract under test:
|
|
6
|
+
* - Gate predicate: enabled + architectureCard + coding context (rule 39).
|
|
7
|
+
* - get: returns not-found when no card exists, found+content when it does.
|
|
8
|
+
* - refresh: builds → persists → versions; first write vs update path.
|
|
9
|
+
* - Build failure → tagged failure, nothing persisted (rule 34/44).
|
|
10
|
+
* - Invalid subcommand → error listing valid options (rule 51).
|
|
11
|
+
*
|
|
12
|
+
* All fixtures are synthetic — no real repos (public-repo policy).
|
|
13
|
+
*/
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import { type EngramAccessService } from "../access-service.js";
|
|
17
|
+
import { getOperation, type OperationName } from "../access-boundary.js";
|
|
18
|
+
import "../access-operations.js";
|
|
19
|
+
import { EngramMcpServer } from "../access-mcp.js";
|
|
20
|
+
import {
|
|
21
|
+
ARCHITECTURE_SUBCOMMANDS,
|
|
22
|
+
ARCHITECTURE_CARD_TAG,
|
|
23
|
+
ARCHITECTURE_CARD_KIND,
|
|
24
|
+
handleCodingArchitecture,
|
|
25
|
+
isArchitectureCardSurfaceEnabled,
|
|
26
|
+
isArchitectureCardSurfaceVisible,
|
|
27
|
+
isArchitectureSubcommand,
|
|
28
|
+
formatArchitectureSubcommands,
|
|
29
|
+
findArchitectureCardMemory,
|
|
30
|
+
type ArchitectureSurfaceContext,
|
|
31
|
+
type ArchitectureSurfaceRequest,
|
|
32
|
+
type ArchitectureSurfaceStorage,
|
|
33
|
+
} from "./architecture-surfaces.js";
|
|
34
|
+
import type { ArchitectureCardBuildResult } from "./architecture-card.js";
|
|
35
|
+
import type {
|
|
36
|
+
CodingKnowledgeConfig,
|
|
37
|
+
CodingContext,
|
|
38
|
+
MemoryFile,
|
|
39
|
+
MemoryFrontmatter,
|
|
40
|
+
} from "../types.js";
|
|
41
|
+
|
|
42
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Fixtures
|
|
44
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const NOW = "2026-07-05T12:00:00.000Z";
|
|
47
|
+
|
|
48
|
+
const DEFAULT_CONFIG: CodingKnowledgeConfig = {
|
|
49
|
+
enabled: true,
|
|
50
|
+
decisionRecords: true,
|
|
51
|
+
architectureCard: true,
|
|
52
|
+
sessionDelta: true,
|
|
53
|
+
architectureCardLlmSummary: false,
|
|
54
|
+
structuralProvider: "none",
|
|
55
|
+
structuralProviderCommand: "",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const CODING_CONTEXT: CodingContext = {
|
|
59
|
+
projectId: "github.com/test/repo",
|
|
60
|
+
branch: "main",
|
|
61
|
+
rootPath: "/synthetic/repo",
|
|
62
|
+
defaultBranch: "main",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function makeFrontmatter(overrides: Partial<MemoryFrontmatter> = {}): MemoryFrontmatter {
|
|
66
|
+
return {
|
|
67
|
+
id: "fact-test-card",
|
|
68
|
+
category: "fact",
|
|
69
|
+
created: NOW,
|
|
70
|
+
updated: NOW,
|
|
71
|
+
source: "coding-architecture",
|
|
72
|
+
confidence: 1.0,
|
|
73
|
+
confidenceTier: "explicit",
|
|
74
|
+
tags: [ARCHITECTURE_CARD_TAG],
|
|
75
|
+
structuredAttributes: { cardKind: ARCHITECTURE_CARD_KIND },
|
|
76
|
+
...overrides,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeCardMemory(overrides: {
|
|
81
|
+
content?: string;
|
|
82
|
+
path?: string;
|
|
83
|
+
frontmatter?: Partial<MemoryFrontmatter>;
|
|
84
|
+
} = {}): MemoryFile {
|
|
85
|
+
const { frontmatter: fmOverrides, content, path: memPath } = overrides;
|
|
86
|
+
return {
|
|
87
|
+
path: memPath ?? "/synthetic/facts/test-card.md",
|
|
88
|
+
frontmatter: makeFrontmatter(fmOverrides),
|
|
89
|
+
content: content ?? "# Architecture Card\n\nsynthetic",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface StubStorage extends ArchitectureSurfaceStorage {
|
|
94
|
+
memories: MemoryFile[];
|
|
95
|
+
written: Array<{ category: string; content: string; options: Record<string, unknown> }>;
|
|
96
|
+
updated: Array<{ id: string; content: string }>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function makeStubStorage(initialMemories: MemoryFile[] = []): StubStorage {
|
|
100
|
+
const allMemories: MemoryFile[] = [...initialMemories];
|
|
101
|
+
const written: Array<{ category: string; content: string; options: Record<string, unknown> }> = [];
|
|
102
|
+
const updated: Array<{ id: string; content: string }> = [];
|
|
103
|
+
return {
|
|
104
|
+
dir: "/synthetic/memory",
|
|
105
|
+
namespace: "default",
|
|
106
|
+
memories: allMemories,
|
|
107
|
+
written,
|
|
108
|
+
updated,
|
|
109
|
+
async readAllMemories() { return [...allMemories]; },
|
|
110
|
+
async writeMemory(category, content, options) {
|
|
111
|
+
const id = `fact-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
112
|
+
written.push({ category, content, options });
|
|
113
|
+
allMemories.push({
|
|
114
|
+
path: `/synthetic/facts/${id}.md`,
|
|
115
|
+
frontmatter: makeFrontmatter({ id, ...(options as Partial<MemoryFrontmatter>) }),
|
|
116
|
+
content,
|
|
117
|
+
});
|
|
118
|
+
return id;
|
|
119
|
+
},
|
|
120
|
+
async updateMemory(id, newContent) {
|
|
121
|
+
updated.push({ id, content: newContent });
|
|
122
|
+
const m = allMemories.find((mem) => mem.frontmatter.id === id);
|
|
123
|
+
if (m) m.content = newContent;
|
|
124
|
+
return !!m;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function makeContext(overrides: Partial<ArchitectureSurfaceContext> = {}): ArchitectureSurfaceContext {
|
|
130
|
+
const storage = makeStubStorage();
|
|
131
|
+
return {
|
|
132
|
+
codingKnowledge: DEFAULT_CONFIG,
|
|
133
|
+
getCodingContext: () => CODING_CONTEXT,
|
|
134
|
+
resolveStorage: async () => storage,
|
|
135
|
+
buildCard: async () => ({
|
|
136
|
+
ok: true,
|
|
137
|
+
card: {
|
|
138
|
+
content: "# Architecture Card\n\nsynthetic",
|
|
139
|
+
generatedAt: NOW,
|
|
140
|
+
byteSize: 40,
|
|
141
|
+
truncated: false,
|
|
142
|
+
},
|
|
143
|
+
}) satisfies ArchitectureCardBuildResult,
|
|
144
|
+
versioning: { snapshotIfExists: async () => {} },
|
|
145
|
+
throwInputError: (msg) => { throw new Error(msg); },
|
|
146
|
+
...overrides,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
151
|
+
// Subcommand validation
|
|
152
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
test("subcommands: isArchitectureSubcommand narrows valid values", () => {
|
|
155
|
+
assert.equal(isArchitectureSubcommand("get"), true);
|
|
156
|
+
assert.equal(isArchitectureSubcommand("refresh"), true);
|
|
157
|
+
assert.equal(isArchitectureSubcommand("delete"), false);
|
|
158
|
+
assert.equal(isArchitectureSubcommand(undefined), false);
|
|
159
|
+
assert.equal(isArchitectureSubcommand(42), false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("subcommands: formatArchitectureSubcommands lists valid options", () => {
|
|
163
|
+
const formatted = formatArchitectureSubcommands();
|
|
164
|
+
for (const sub of ARCHITECTURE_SUBCOMMANDS) {
|
|
165
|
+
assert.ok(formatted.includes(sub), `includes ${sub}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
170
|
+
// Gate predicates
|
|
171
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
test("gate: surface enabled when config + architectureCard + coding context all true", () => {
|
|
174
|
+
assert.equal(isArchitectureCardSurfaceEnabled(DEFAULT_CONFIG, CODING_CONTEXT), true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("gate: surface disabled when master gate off", () => {
|
|
178
|
+
const config: CodingKnowledgeConfig = { ...DEFAULT_CONFIG, enabled: false };
|
|
179
|
+
assert.equal(isArchitectureCardSurfaceEnabled(config, CODING_CONTEXT), false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("gate: surface disabled when architectureCard off", () => {
|
|
183
|
+
const config: CodingKnowledgeConfig = { ...DEFAULT_CONFIG, architectureCard: false };
|
|
184
|
+
assert.equal(isArchitectureCardSurfaceEnabled(config, CODING_CONTEXT), false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("gate: surface disabled when no coding context", () => {
|
|
188
|
+
assert.equal(isArchitectureCardSurfaceEnabled(DEFAULT_CONFIG, null), false);
|
|
189
|
+
assert.equal(isArchitectureCardSurfaceEnabled(DEFAULT_CONFIG, undefined), false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("gate: visibility (tools/list) matches config-only check", () => {
|
|
193
|
+
assert.equal(isArchitectureCardSurfaceVisible(DEFAULT_CONFIG), true);
|
|
194
|
+
assert.equal(isArchitectureCardSurfaceVisible({ ...DEFAULT_CONFIG, enabled: false }), false);
|
|
195
|
+
assert.equal(isArchitectureCardSurfaceVisible({ ...DEFAULT_CONFIG, architectureCard: false }), false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
199
|
+
// Handler gate — throws when gate fails
|
|
200
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
test("handler: throws when gate is off (no coding context)", async () => {
|
|
203
|
+
const ctx = makeContext({ getCodingContext: () => null });
|
|
204
|
+
const request: ArchitectureSurfaceRequest = { subcommand: "get", sessionKey: "s1" };
|
|
205
|
+
await assert.rejects(
|
|
206
|
+
() => handleCodingArchitecture(request, ctx),
|
|
207
|
+
/coding_architecture requires/,
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
212
|
+
// get — not-found when no card exists
|
|
213
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
test("get: returns found=false when no card exists in namespace", async () => {
|
|
216
|
+
const ctx = makeContext();
|
|
217
|
+
const result = await handleCodingArchitecture({ subcommand: "get", sessionKey: "s1" }, ctx);
|
|
218
|
+
assert.equal(result.subcommand, "get");
|
|
219
|
+
if (result.subcommand !== "get") return;
|
|
220
|
+
assert.equal(result.found, false);
|
|
221
|
+
assert.equal(result.card, undefined);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
225
|
+
// get — found when card exists
|
|
226
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
test("get: returns found=true with card content when card exists", async () => {
|
|
229
|
+
const cardContent = "# Architecture Card\n\nexisting content";
|
|
230
|
+
const storage = makeStubStorage([makeCardMemory({ content: cardContent })]);
|
|
231
|
+
const ctx = makeContext({ resolveStorage: async () => storage });
|
|
232
|
+
const result = await handleCodingArchitecture({ subcommand: "get", sessionKey: "s1" }, ctx);
|
|
233
|
+
assert.equal(result.subcommand, "get");
|
|
234
|
+
if (result.subcommand !== "get") return;
|
|
235
|
+
assert.equal(result.found, true);
|
|
236
|
+
assert.ok(result.card);
|
|
237
|
+
assert.equal(result.card!.content, cardContent);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("get: strips the [Attributes:] suffix writeMemory appends (cursor review)", async () => {
|
|
241
|
+
// writeMemory appends `\n[Attributes: cardKind=architecture]` to the stored
|
|
242
|
+
// body when structuredAttributes are present. The card markdown returned to
|
|
243
|
+
// clients must NOT include that storage-metadata suffix, and byteSize must
|
|
244
|
+
// reflect the card alone.
|
|
245
|
+
const cardMarkdown = "# Architecture Card\n\nreal content";
|
|
246
|
+
const storedContent = `${cardMarkdown}\n[Attributes: cardKind=architecture]`;
|
|
247
|
+
const storage = makeStubStorage([makeCardMemory({ content: storedContent })]);
|
|
248
|
+
const ctx = makeContext({ resolveStorage: async () => storage });
|
|
249
|
+
const result = await handleCodingArchitecture({ subcommand: "get", sessionKey: "s1" }, ctx);
|
|
250
|
+
if (result.subcommand !== "get") return;
|
|
251
|
+
assert.equal(result.found, true);
|
|
252
|
+
assert.ok(result.card);
|
|
253
|
+
assert.equal(result.card!.content, cardMarkdown, "attributes suffix stripped from get content");
|
|
254
|
+
assert.equal(result.card!.byteSize, Buffer.byteLength(cardMarkdown, "utf-8"), "byteSize excludes the suffix");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("get: shares storage's stripper — legitimate trailing attributes-like line is not truncated (cursor review)", async () => {
|
|
258
|
+
// The handler now uses the shared `stripAttributesSuffix` from
|
|
259
|
+
// structured-attributes.ts (dedup). That helper only strips a single-line
|
|
260
|
+
// enrichment whose payload has no premature `]` or embedded newline — so
|
|
261
|
+
// card markdown that legitimately ends in an attributes-LIKE line (e.g. one
|
|
262
|
+
// quoting a `[config]` token) is returned verbatim. The old local copy only
|
|
263
|
+
// checked the trailing `]` and would have sliced this content in half.
|
|
264
|
+
const cardMarkdown =
|
|
265
|
+
"# Architecture Card\n\nreal content\n[Attributes: see the [config] section]";
|
|
266
|
+
const storage = makeStubStorage([makeCardMemory({ content: cardMarkdown })]);
|
|
267
|
+
const ctx = makeContext({ resolveStorage: async () => storage });
|
|
268
|
+
const result = await handleCodingArchitecture({ subcommand: "get", sessionKey: "s1" }, ctx);
|
|
269
|
+
if (result.subcommand !== "get") return;
|
|
270
|
+
assert.equal(result.found, true);
|
|
271
|
+
assert.ok(result.card);
|
|
272
|
+
assert.equal(result.card!.content, cardMarkdown, "legitimate attributes-like line preserved, not truncated");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
276
|
+
// refresh — first write (no existing card)
|
|
277
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
test("refresh: first card written via writeMemory, no version snapshot", async () => {
|
|
280
|
+
const storage = makeStubStorage();
|
|
281
|
+
const snapshots: MemoryFile[] = [];
|
|
282
|
+
const ctx = makeContext({
|
|
283
|
+
resolveStorage: async () => storage,
|
|
284
|
+
versioning: {
|
|
285
|
+
snapshotIfExists: async (m) => { snapshots.push(m); },
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
const result = await handleCodingArchitecture({ subcommand: "refresh", sessionKey: "s1" }, ctx);
|
|
289
|
+
assert.equal(result.subcommand, "refresh");
|
|
290
|
+
if (result.subcommand !== "refresh") return;
|
|
291
|
+
assert.equal(result.refreshed, true);
|
|
292
|
+
assert.ok(result.memoryId, "memoryId returned");
|
|
293
|
+
assert.equal(storage.written.length, 1, "one writeMemory call");
|
|
294
|
+
assert.equal(storage.updated.length, 0, "no updateMemory call");
|
|
295
|
+
assert.equal(snapshots.length, 0, "no version snapshot on first write (nothing to preserve)");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
299
|
+
// refresh — update existing card (snapshot before overwrite, rule 25)
|
|
300
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
test("refresh: existing card snapshotted before update (rule 25)", async () => {
|
|
303
|
+
const oldCard = makeCardMemory({ content: "# OLD CARD" });
|
|
304
|
+
const storage = makeStubStorage([oldCard]);
|
|
305
|
+
const snapshots: MemoryFile[] = [];
|
|
306
|
+
const ctx = makeContext({
|
|
307
|
+
resolveStorage: async () => storage,
|
|
308
|
+
versioning: {
|
|
309
|
+
snapshotIfExists: async (m) => { snapshots.push(m); },
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
const result = await handleCodingArchitecture({ subcommand: "refresh", sessionKey: "s1" }, ctx);
|
|
313
|
+
assert.equal(result.subcommand, "refresh");
|
|
314
|
+
if (result.subcommand !== "refresh") return;
|
|
315
|
+
assert.equal(result.refreshed, true);
|
|
316
|
+
assert.equal(result.memoryId, oldCard.frontmatter.id, "same memory id (updated in place)");
|
|
317
|
+
assert.equal(snapshots.length, 1, "prior card snapshotted before update");
|
|
318
|
+
assert.equal(snapshots[0]!.frontmatter.id, oldCard.frontmatter.id, "snapshot is of the old card");
|
|
319
|
+
assert.equal(storage.updated.length, 1, "one updateMemory call");
|
|
320
|
+
assert.equal(storage.written.length, 0, "no writeMemory call (update path)");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
324
|
+
// refresh — build failure → tagged failure, nothing persisted
|
|
325
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
test("refresh: build failure throws, nothing persisted (rule 34/44)", async () => {
|
|
328
|
+
const storage = makeStubStorage();
|
|
329
|
+
const ctx = makeContext({
|
|
330
|
+
resolveStorage: async () => storage,
|
|
331
|
+
buildCard: async () => ({
|
|
332
|
+
ok: false,
|
|
333
|
+
code: "scan_failed",
|
|
334
|
+
detail: "permission denied",
|
|
335
|
+
}) satisfies ArchitectureCardBuildResult,
|
|
336
|
+
});
|
|
337
|
+
await assert.rejects(
|
|
338
|
+
() => handleCodingArchitecture({ subcommand: "refresh", sessionKey: "s1" }, ctx),
|
|
339
|
+
(err: Error) => {
|
|
340
|
+
assert.match(err.message, /scan_failed/, "error carries the build code");
|
|
341
|
+
assert.doesNotMatch(
|
|
342
|
+
err.message,
|
|
343
|
+
/permission denied/,
|
|
344
|
+
"raw detail must NOT reach the client (cursor review: raw build errors)",
|
|
345
|
+
);
|
|
346
|
+
return true;
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
assert.equal(storage.written.length, 0, "nothing written on build failure");
|
|
350
|
+
assert.equal(storage.updated.length, 0, "nothing updated on build failure");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("refresh: invalid_root build failure surfaces the code", async () => {
|
|
354
|
+
const ctx = makeContext({
|
|
355
|
+
buildCard: async () => ({
|
|
356
|
+
ok: false,
|
|
357
|
+
code: "invalid_root",
|
|
358
|
+
detail: "not a directory",
|
|
359
|
+
}) satisfies ArchitectureCardBuildResult,
|
|
360
|
+
});
|
|
361
|
+
await assert.rejects(
|
|
362
|
+
() => handleCodingArchitecture({ subcommand: "refresh", sessionKey: "s1" }, ctx),
|
|
363
|
+
/invalid_root/,
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
368
|
+
// refresh — truncated card carries the truncation tag
|
|
369
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
test("refresh: truncated card carries 'truncated' tag in write options", async () => {
|
|
372
|
+
const storage = makeStubStorage();
|
|
373
|
+
const ctx = makeContext({
|
|
374
|
+
resolveStorage: async () => storage,
|
|
375
|
+
buildCard: async () => ({
|
|
376
|
+
ok: true,
|
|
377
|
+
card: {
|
|
378
|
+
content: "# truncated card",
|
|
379
|
+
generatedAt: NOW,
|
|
380
|
+
byteSize: 4096,
|
|
381
|
+
truncated: true,
|
|
382
|
+
},
|
|
383
|
+
}) satisfies ArchitectureCardBuildResult,
|
|
384
|
+
});
|
|
385
|
+
const result = await handleCodingArchitecture({ subcommand: "refresh", sessionKey: "s1" }, ctx);
|
|
386
|
+
if (result.subcommand !== "refresh") return;
|
|
387
|
+
assert.equal(result.truncated, true);
|
|
388
|
+
// Tags are passed in the writeMemory options and the stub stores them in frontmatter.
|
|
389
|
+
assert.deepEqual(storage.written[0]!.options.tags, [ARCHITECTURE_CARD_TAG, "truncated"]);
|
|
390
|
+
const written = storage.memories[storage.memories.length - 1];
|
|
391
|
+
assert.ok(written?.frontmatter.tags?.includes("truncated"), "truncated tag present");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
395
|
+
// get — truncation derived from content, not from a stale tag (cursor review)
|
|
396
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
test("get: truncation derived from content marker, surviving an update that left stale tags", async () => {
|
|
399
|
+
// Seed a card that was originally truncated (tag present) but whose
|
|
400
|
+
// content was later updated to a non-truncated body via updateMemory
|
|
401
|
+
// (which does not refresh frontmatter tags).
|
|
402
|
+
const storage = makeStubStorage([
|
|
403
|
+
makeCardMemory({
|
|
404
|
+
content: "# Fresh card — no truncation marker",
|
|
405
|
+
frontmatter: { tags: [ARCHITECTURE_CARD_TAG, "truncated"] },
|
|
406
|
+
}),
|
|
407
|
+
]);
|
|
408
|
+
const ctx = makeContext({ resolveStorage: async () => storage });
|
|
409
|
+
const result = await handleCodingArchitecture({ subcommand: "get", sessionKey: "s1" }, ctx);
|
|
410
|
+
if (result.subcommand !== "get") return;
|
|
411
|
+
assert.equal(result.found, true);
|
|
412
|
+
assert.equal(result.card?.truncated, false, "content has no marker → false despite stale tag");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("get: truncation true when content carries the marker", async () => {
|
|
416
|
+
const storage = makeStubStorage([
|
|
417
|
+
makeCardMemory({
|
|
418
|
+
content: "# Card\n\n_… card truncated to fit size cap …_",
|
|
419
|
+
frontmatter: { tags: [ARCHITECTURE_CARD_TAG] },
|
|
420
|
+
}),
|
|
421
|
+
]);
|
|
422
|
+
const ctx = makeContext({ resolveStorage: async () => storage });
|
|
423
|
+
const result = await handleCodingArchitecture({ subcommand: "get", sessionKey: "s1" }, ctx);
|
|
424
|
+
if (result.subcommand !== "get") return;
|
|
425
|
+
assert.equal(result.found, true);
|
|
426
|
+
assert.equal(result.card?.truncated, true, "content marker present → true");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
430
|
+
// findArchitectureCardMemory — helper
|
|
431
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
test("findArchitectureCardMemory: returns null when no card in namespace", async () => {
|
|
434
|
+
const storage = makeStubStorage([
|
|
435
|
+
makeCardMemory({ frontmatter: { category: "decision" } }),
|
|
436
|
+
]);
|
|
437
|
+
const result = await findArchitectureCardMemory(storage);
|
|
438
|
+
assert.equal(result, null);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("findArchitectureCardMemory: skips retired cards (non-active status)", async () => {
|
|
442
|
+
const storage = makeStubStorage([
|
|
443
|
+
makeCardMemory({ frontmatter: { status: "archived" } }),
|
|
444
|
+
]);
|
|
445
|
+
const result = await findArchitectureCardMemory(storage);
|
|
446
|
+
assert.equal(result, null, "archived card is not found");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("findArchitectureCardMemory: returns the most recently updated card", async () => {
|
|
450
|
+
const older = makeCardMemory({
|
|
451
|
+
frontmatter: { id: "old", updated: "2026-01-01T00:00:00.000Z" },
|
|
452
|
+
});
|
|
453
|
+
const newer = makeCardMemory({
|
|
454
|
+
frontmatter: { id: "new", updated: "2026-07-01T00:00:00.000Z" },
|
|
455
|
+
});
|
|
456
|
+
const storage = makeStubStorage([older, newer]);
|
|
457
|
+
const result = await findArchitectureCardMemory(storage);
|
|
458
|
+
assert.equal(result?.frontmatter.id, "new");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("findArchitectureCardMemory: rejects tagged fact without cardKind marker (codex review)", async () => {
|
|
462
|
+
// A user-created fact that merely happens to be tagged "architecture-card"
|
|
463
|
+
// must NOT be treated as the managed card (would be overwritten on refresh).
|
|
464
|
+
const storage = makeStubStorage([
|
|
465
|
+
makeCardMemory({ frontmatter: { structuredAttributes: {} } }),
|
|
466
|
+
]);
|
|
467
|
+
const result = await findArchitectureCardMemory(storage);
|
|
468
|
+
assert.equal(result, null, "tagged fact without cardKind=architecture is not the managed card");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("findArchitectureCardMemory: skips cards with archivedAt set (cursor review)", async () => {
|
|
472
|
+
const storage = makeStubStorage([
|
|
473
|
+
makeCardMemory({ frontmatter: { archivedAt: "2026-01-01T00:00:00.000Z" } }),
|
|
474
|
+
]);
|
|
475
|
+
const result = await findArchitectureCardMemory(storage);
|
|
476
|
+
assert.equal(result, null, "card with archivedAt is not found even without explicit status");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
480
|
+
// Prove-fail-before: tools/list gate (rule 39)
|
|
481
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Type-guarded extraction of tool names from a tools/list response.
|
|
485
|
+
* Narrows with `in` checks so no inline cast is needed (no-fabricated-shape
|
|
486
|
+
* rule). Returns the empty set on any shape the guard rejects.
|
|
487
|
+
*/
|
|
488
|
+
function extractToolNames(response: unknown): Set<string> {
|
|
489
|
+
const names = new Set<string>();
|
|
490
|
+
if (typeof response !== "object" || response === null) return names;
|
|
491
|
+
if (!("result" in response)) return names;
|
|
492
|
+
const result = response.result;
|
|
493
|
+
if (typeof result !== "object" || result === null || Array.isArray(result)) return names;
|
|
494
|
+
if (!("tools" in result)) return names;
|
|
495
|
+
const tools = result.tools;
|
|
496
|
+
if (!Array.isArray(tools)) return names;
|
|
497
|
+
for (const tool of tools) {
|
|
498
|
+
if (tool !== null && typeof tool === "object" && "name" in tool && typeof tool.name === "string") {
|
|
499
|
+
names.add(tool.name);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return names;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
test("tools/list: engram.coding_architecture absent when gate off (byte-identical to main)", async () => {
|
|
506
|
+
const stub = { briefingEnabled: true } as unknown as EngramAccessService;
|
|
507
|
+
const server = new EngramMcpServer(stub, { emitLegacyTools: true });
|
|
508
|
+
const response = await server.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
509
|
+
const names = extractToolNames(response);
|
|
510
|
+
assert.equal(names.has("engram.coding_architecture"), false, "tool must be absent when gate is off");
|
|
511
|
+
assert.equal(names.has("remnic.coding_architecture"), false, "alias must be absent when gate is off");
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("tools/list: engram.coding_architecture present when gate on", async () => {
|
|
515
|
+
const stub = { briefingEnabled: true } as unknown as EngramAccessService;
|
|
516
|
+
const server = new EngramMcpServer(stub, { emitLegacyTools: true, architectureCardVisible: true });
|
|
517
|
+
const response = await server.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
518
|
+
const names = extractToolNames(response);
|
|
519
|
+
assert.equal(names.has("engram.coding_architecture"), true, "tool must be advertised when gate is on");
|
|
520
|
+
assert.equal(names.has("remnic.coding_architecture"), true, "alias must be advertised when gate is on");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
524
|
+
// Registry fitness: coding_architecture operation is registered
|
|
525
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
test("registry: coding_architecture operation is registered through the boundary", () => {
|
|
528
|
+
const op = getOperation("coding_architecture" as OperationName);
|
|
529
|
+
assert.ok(op, "coding_architecture must be registered in the operation registry");
|
|
530
|
+
assert.equal(op?.spec.name, "coding_architecture");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
534
|
+
// Three surfaces → one service method (rule 22 spirit)
|
|
535
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
test("MCP surface: engram.coding_architecture dispatches through the boundary to service.codingArchitecture", async () => {
|
|
538
|
+
const calls: ArchitectureSurfaceRequest[] = [];
|
|
539
|
+
const service = {
|
|
540
|
+
codingArchitecture(req: ArchitectureSurfaceRequest): Promise<unknown> {
|
|
541
|
+
calls.push(req);
|
|
542
|
+
return Promise.resolve({ subcommand: "get", found: false });
|
|
543
|
+
},
|
|
544
|
+
} as unknown as EngramAccessService;
|
|
545
|
+
const server = new EngramMcpServer(service, { emitLegacyTools: true, architectureCardVisible: true });
|
|
546
|
+
await server.handleRequest({
|
|
547
|
+
jsonrpc: "2.0",
|
|
548
|
+
id: 1,
|
|
549
|
+
method: "tools/call",
|
|
550
|
+
params: {
|
|
551
|
+
name: "engram.coding_architecture",
|
|
552
|
+
arguments: { subcommand: "get", sessionKey: "s1" },
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
assert.equal(calls.length, 1, "service.codingArchitecture called exactly once");
|
|
556
|
+
assert.equal(calls[0]?.subcommand, "get");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("MCP surface: remnic.coding_architecture alias dispatches identically", async () => {
|
|
560
|
+
const calls: ArchitectureSurfaceRequest[] = [];
|
|
561
|
+
const service = {
|
|
562
|
+
codingArchitecture(req: ArchitectureSurfaceRequest): Promise<unknown> {
|
|
563
|
+
calls.push(req);
|
|
564
|
+
return Promise.resolve({ subcommand: "get", found: false });
|
|
565
|
+
},
|
|
566
|
+
} as unknown as EngramAccessService;
|
|
567
|
+
const server = new EngramMcpServer(service, { emitLegacyTools: true, architectureCardVisible: true });
|
|
568
|
+
await server.handleRequest({
|
|
569
|
+
jsonrpc: "2.0",
|
|
570
|
+
id: 2,
|
|
571
|
+
method: "tools/call",
|
|
572
|
+
params: {
|
|
573
|
+
name: "remnic.coding_architecture",
|
|
574
|
+
arguments: { subcommand: "get", sessionKey: "s1" },
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
assert.equal(calls.length, 1, "service.codingArchitecture called exactly once via alias");
|
|
578
|
+
assert.equal(calls[0]?.subcommand, "get");
|
|
579
|
+
});
|