@remnic/core 9.3.671 → 9.3.673

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 (98) hide show
  1. package/dist/access-audit.js +2 -2
  2. package/dist/access-cli.js +27 -25
  3. package/dist/access-cli.js.map +1 -1
  4. package/dist/access-http.js +12 -12
  5. package/dist/access-mcp.js +11 -11
  6. package/dist/access-schema.d.ts +36 -36
  7. package/dist/access-schema.js +3 -3
  8. package/dist/access-service.js +9 -9
  9. package/dist/active-recall.js +3 -1
  10. package/dist/active-recall.js.map +1 -1
  11. package/dist/chunk-3BQOQYRB.js +33 -0
  12. package/dist/chunk-3BQOQYRB.js.map +1 -0
  13. package/dist/{chunk-UOBLE67F.js → chunk-3IE22DJ2.js} +4 -4
  14. package/dist/chunk-52LZ42LI.js +25 -0
  15. package/dist/chunk-52LZ42LI.js.map +1 -0
  16. package/dist/{chunk-CRO4LCQ6.js → chunk-7OGJQP7T.js} +5 -5
  17. package/dist/{chunk-23EBQ27U.js → chunk-B55KFEGS.js} +2 -2
  18. package/dist/{chunk-BTLNC5YM.js → chunk-GNAMDNGT.js} +5 -13
  19. package/dist/chunk-GNAMDNGT.js.map +1 -0
  20. package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
  21. package/dist/{chunk-MLVMBV2C.js → chunk-IUZWBCJX.js} +8 -40
  22. package/dist/chunk-IUZWBCJX.js.map +1 -0
  23. package/dist/{chunk-PYTATYUV.js → chunk-ODWI5XU2.js} +2 -2
  24. package/dist/{chunk-F7OWUP3G.js → chunk-OG7A6AZX.js} +4 -4
  25. package/dist/{chunk-XS2CWEHZ.js → chunk-Q6MIDQEL.js} +2 -2
  26. package/dist/{chunk-UDJLF3BO.js → chunk-QLRYXOAD.js} +2 -2
  27. package/dist/chunk-QO3AILZN.js +89 -0
  28. package/dist/chunk-QO3AILZN.js.map +1 -0
  29. package/dist/{chunk-ZI6A7X4V.js → chunk-R37A3BEW.js} +26 -26
  30. package/dist/{chunk-CPVV2UEL.js → chunk-SDLJ2W7S.js} +6 -6
  31. package/dist/{chunk-Z4GALEO3.js → chunk-SF45RQDX.js} +3 -3
  32. package/dist/{chunk-LXVOZ2O6.js → chunk-T2AOOHDA.js} +2 -2
  33. package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
  34. package/dist/{chunk-32RD3GIW.js → chunk-XVVEKF5I.js} +19 -19
  35. package/dist/{chunk-TGN4M5MB.js → chunk-ZLINDOBG.js} +4 -4
  36. package/dist/cli.js +24 -22
  37. package/dist/coding/optional-coding-graph.d.ts +63 -0
  38. package/dist/coding/optional-coding-graph.js +119 -0
  39. package/dist/coding/optional-coding-graph.js.map +1 -0
  40. package/dist/coding-graph-types-Dd2tGrnm.d.ts +106 -0
  41. package/dist/config.d.ts +1 -1
  42. package/dist/config.js +3 -1
  43. package/dist/connectors/index.d.ts +6 -2
  44. package/dist/connectors/index.js +6 -2
  45. package/dist/conversation-index/backend.js +2 -2
  46. package/dist/emit-legacy-tools.d.ts +61 -0
  47. package/dist/emit-legacy-tools.js +12 -0
  48. package/dist/emit-legacy-tools.js.map +1 -0
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +54 -46
  51. package/dist/index.js.map +1 -1
  52. package/dist/lcm/engine.js +2 -2
  53. package/dist/lcm/index.js +2 -2
  54. package/dist/namespaces/migrate.js +5 -5
  55. package/dist/namespaces/search.js +4 -4
  56. package/dist/operator-toolkit.js +10 -8
  57. package/dist/orchestrator.js +16 -16
  58. package/dist/resume-bundles.js +4 -2
  59. package/dist/schemas.d.ts +42 -42
  60. package/dist/search/factory.js +3 -3
  61. package/dist/search/index.js +3 -3
  62. package/dist/shared-context/manager.d.ts +2 -2
  63. package/dist/transfer/autodetect.js +1 -1
  64. package/dist/transfer/backup.js +1 -1
  65. package/dist/transfer/capsule-export.js +2 -2
  66. package/package.json +19 -1
  67. package/src/coding/coding-graph-types.ts +180 -0
  68. package/src/coding/optional-coding-graph-cache.test.ts +86 -0
  69. package/src/coding/optional-coding-graph-cacheread.test.ts +78 -0
  70. package/src/coding/optional-coding-graph-concurrent.test.ts +48 -0
  71. package/src/coding/optional-coding-graph-incompatible.test.ts +98 -0
  72. package/src/coding/optional-coding-graph-probe.test.ts +34 -0
  73. package/src/coding/optional-coding-graph.test.ts +117 -0
  74. package/src/coding/optional-coding-graph.ts +370 -0
  75. package/src/config.test.ts +408 -6
  76. package/src/config.ts +12 -56
  77. package/src/connectors/index.ts +2 -15
  78. package/src/connectors/paths.ts +50 -0
  79. package/src/emit-legacy-tools.test.ts +297 -0
  80. package/src/emit-legacy-tools.ts +204 -0
  81. package/src/index.ts +22 -0
  82. package/dist/chunk-BTLNC5YM.js.map +0 -1
  83. package/dist/chunk-MLVMBV2C.js.map +0 -1
  84. /package/dist/{chunk-UOBLE67F.js.map → chunk-3IE22DJ2.js.map} +0 -0
  85. /package/dist/{chunk-CRO4LCQ6.js.map → chunk-7OGJQP7T.js.map} +0 -0
  86. /package/dist/{chunk-23EBQ27U.js.map → chunk-B55KFEGS.js.map} +0 -0
  87. /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
  88. /package/dist/{chunk-PYTATYUV.js.map → chunk-ODWI5XU2.js.map} +0 -0
  89. /package/dist/{chunk-F7OWUP3G.js.map → chunk-OG7A6AZX.js.map} +0 -0
  90. /package/dist/{chunk-XS2CWEHZ.js.map → chunk-Q6MIDQEL.js.map} +0 -0
  91. /package/dist/{chunk-UDJLF3BO.js.map → chunk-QLRYXOAD.js.map} +0 -0
  92. /package/dist/{chunk-ZI6A7X4V.js.map → chunk-R37A3BEW.js.map} +0 -0
  93. /package/dist/{chunk-CPVV2UEL.js.map → chunk-SDLJ2W7S.js.map} +0 -0
  94. /package/dist/{chunk-Z4GALEO3.js.map → chunk-SF45RQDX.js.map} +0 -0
  95. /package/dist/{chunk-LXVOZ2O6.js.map → chunk-T2AOOHDA.js.map} +0 -0
  96. /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
  97. /package/dist/{chunk-32RD3GIW.js.map → chunk-XVVEKF5I.js.map} +0 -0
  98. /package/dist/{chunk-TGN4M5MB.js.map → chunk-ZLINDOBG.js.map} +0 -0
@@ -0,0 +1,297 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import {
8
+ resolveEmitLegacyTools,
9
+ resolveNamespaceCatalogEnabled,
10
+ } from "./emit-legacy-tools.js";
11
+
12
+ /**
13
+ * Run `body` with XDG_CONFIG_HOME pointing at a throwaway dir so the
14
+ * sticky-legacy `hasLegacyConnectorEntries()` check inside the resolvers
15
+ * never reads the real machine state. `withLegacyEntry` seeds one
16
+ * persisted connector file under the standard
17
+ * `$XDG_CONFIG_HOME/engram/.engram-connectors/connectors/` layout.
18
+ */
19
+ function withIsolatedConnectorsDir<T>(
20
+ withLegacyEntry: boolean,
21
+ body: () => T,
22
+ ): T {
23
+ const prev = process.env.XDG_CONFIG_HOME;
24
+ const root = mkdtempSync(path.join(tmpdir(), "emit-legacy-tools-test-"));
25
+ process.env.XDG_CONFIG_HOME = root;
26
+ try {
27
+ if (withLegacyEntry) {
28
+ const dir = path.join(root, "engram", ".engram-connectors", "connectors");
29
+ mkdirSync(dir, { recursive: true });
30
+ writeFileSync(path.join(dir, "codex-cli.json"), "{}\n");
31
+ }
32
+ return body();
33
+ } finally {
34
+ if (prev === undefined) delete process.env.XDG_CONFIG_HOME;
35
+ else process.env.XDG_CONFIG_HOME = prev;
36
+ rmSync(root, { recursive: true, force: true });
37
+ }
38
+ }
39
+
40
+ // ============================================================================
41
+ // resolveEmitLegacyTools
42
+ // ============================================================================
43
+
44
+ test("resolveEmitLegacyTools: fresh install with absent raw falls through to sticky-legacy (false)", () => {
45
+ // No legacy connector evidence on disk → sticky-legacy returns false.
46
+ withIsolatedConnectorsDir(false, () => {
47
+ assert.equal(resolveEmitLegacyTools(false, {}), false);
48
+ // Same shape with legacy connector evidence → sticky-legacy true.
49
+ });
50
+ withIsolatedConnectorsDir(true, () => {
51
+ assert.equal(resolveEmitLegacyTools(false, {}), true);
52
+ });
53
+ });
54
+
55
+ test("resolveEmitLegacyTools: explicit operator opt-out via raw honored", () => {
56
+ // Raw has the key with a real value (false) and configValue agrees.
57
+ withIsolatedConnectorsDir(false, () => {
58
+ assert.equal(
59
+ resolveEmitLegacyTools(false, { emitLegacyTools: false }),
60
+ false,
61
+ "raw operator opt-out via file is honored",
62
+ );
63
+ // Even on a sticky-legacy true install, raw false wins.
64
+ });
65
+ withIsolatedConnectorsDir(true, () => {
66
+ assert.equal(
67
+ resolveEmitLegacyTools(false, { emitLegacyTools: false }),
68
+ false,
69
+ "raw operator opt-out overrides sticky-legacy",
70
+ );
71
+ });
72
+ });
73
+
74
+ test("resolveEmitLegacyTools: raw null/undefined treated as absent (round-2 review)", () => {
75
+ withIsolatedConnectorsDir(false, () => {
76
+ // Fresh install: raw null + merged null → sticky-legacy false.
77
+ assert.equal(
78
+ resolveEmitLegacyTools(null, { emitLegacyTools: null }),
79
+ false,
80
+ "fresh install with raw null resolves to false via sticky-legacy",
81
+ );
82
+ assert.equal(
83
+ resolveEmitLegacyTools(undefined, { emitLegacyTools: undefined }),
84
+ false,
85
+ "fresh install with raw undefined resolves to false via sticky-legacy",
86
+ );
87
+ assert.equal(
88
+ resolveEmitLegacyTools(undefined, { emitLegacyTools: null }),
89
+ false,
90
+ "mixed null/undefined in raw resolves to false via sticky-legacy",
91
+ );
92
+ });
93
+ withIsolatedConnectorsDir(true, () => {
94
+ // Upgraded install: raw null + sticky evidence → true.
95
+ assert.equal(
96
+ resolveEmitLegacyTools(null, { emitLegacyTools: null }),
97
+ true,
98
+ "upgraded install with raw null resolves to true via sticky-legacy",
99
+ );
100
+ assert.equal(
101
+ resolveEmitLegacyTools(undefined, { emitLegacyTools: undefined }),
102
+ true,
103
+ "upgraded install with raw undefined resolves to true via sticky-legacy",
104
+ );
105
+ });
106
+ });
107
+
108
+ test("resolveEmitLegacyTools: runtime true overrides schema default (round-2 review)", () => {
109
+ // Schema default is `false`. When raw is missing the key but merged
110
+ // carries `true` (runtime gateway set it), the resolver honors the
111
+ // runtime override instead of dropping it as schema-default
112
+ // materialization.
113
+ withIsolatedConnectorsDir(false, () => {
114
+ assert.equal(
115
+ resolveEmitLegacyTools(true, {}),
116
+ true,
117
+ "merged true with empty raw treated as runtime operator intent",
118
+ );
119
+ // Merged false (the schema default) with empty raw → sticky fallback.
120
+ assert.equal(
121
+ resolveEmitLegacyTools(false, {}),
122
+ false,
123
+ "merged false (schema default) with empty raw falls through to sticky-legacy",
124
+ );
125
+ });
126
+ });
127
+
128
+ test("resolveEmitLegacyTools: null raw passed directly (not via undefined) is normalized (round-3 review)", () => {
129
+ withIsolatedConnectorsDir(false, () => {
130
+ // Null raw + merged schema-default false → sticky-legacy fallback (no throw).
131
+ assert.equal(
132
+ resolveEmitLegacyTools(false, null),
133
+ false,
134
+ "null raw with emitLegacyTools merged schema default falls through to sticky-legacy",
135
+ );
136
+ // Null raw + emitLegacyTools merged true (runtime intent) → honored.
137
+ assert.equal(
138
+ resolveEmitLegacyTools(true, null),
139
+ true,
140
+ "null raw with emitLegacyTools runtime true honored as operator intent",
141
+ );
142
+ });
143
+ withIsolatedConnectorsDir(true, () => {
144
+ assert.equal(
145
+ resolveEmitLegacyTools(false, null),
146
+ true,
147
+ "null raw with legacy evidence keeps aliases on",
148
+ );
149
+ });
150
+ });
151
+
152
+ test("resolveEmitLegacyTools: runtime-over-file precedence (round-4 review)", () => {
153
+ // File says false, runtime sets true → merged configValue (true) wins.
154
+ withIsolatedConnectorsDir(false, () => {
155
+ assert.equal(
156
+ resolveEmitLegacyTools(true, { emitLegacyTools: false }),
157
+ true,
158
+ "runtime true overrides file false (rawOperatorConfig has the key)",
159
+ );
160
+ // Symmetric: file says true, runtime says false → merged false wins.
161
+ assert.equal(
162
+ resolveEmitLegacyTools(false, { emitLegacyTools: true }),
163
+ false,
164
+ "runtime false overrides file true",
165
+ );
166
+ });
167
+ });
168
+
169
+ test("resolveEmitLegacyTools: raw has key with non-boolean value coerced or throws", () => {
170
+ // Boolean-like strings coerce; garbage throws via coerceBooleanLikeOrThrow.
171
+ assert.equal(
172
+ resolveEmitLegacyTools("true", {}),
173
+ true,
174
+ "string 'true' coerces to true",
175
+ );
176
+ assert.equal(
177
+ resolveEmitLegacyTools("false", {}),
178
+ false,
179
+ "string 'false' coerces to false",
180
+ );
181
+ assert.equal(
182
+ resolveEmitLegacyTools(1, {}),
183
+ true,
184
+ "number 1 coerces to true",
185
+ );
186
+ assert.equal(
187
+ resolveEmitLegacyTools(0, {}),
188
+ false,
189
+ "number 0 coerces to false",
190
+ );
191
+ assert.throws(
192
+ () => resolveEmitLegacyTools("maybe", {}),
193
+ /emitLegacyTools must be a boolean-like value/,
194
+ "unrecognized string throws via coerceBooleanLikeOrThrow",
195
+ );
196
+ assert.throws(
197
+ () => resolveEmitLegacyTools(2, {}),
198
+ /emitLegacyTools must be a boolean-like value/,
199
+ "unrecognized number throws via coerceBooleanLikeOrThrow",
200
+ );
201
+ });
202
+
203
+ test("resolveEmitLegacyTools: env var overrides (REMNIC_ preferred, ENGRAM_ legacy)", () => {
204
+ const prevRemnic = process.env.REMNIC_EMIT_LEGACY_TOOLS;
205
+ const prevEngram = process.env.ENGRAM_EMIT_LEGACY_TOOLS;
206
+ try {
207
+ process.env.REMNIC_EMIT_LEGACY_TOOLS = "false";
208
+ assert.equal(resolveEmitLegacyTools(undefined, undefined), false);
209
+ process.env.REMNIC_EMIT_LEGACY_TOOLS = "true";
210
+ assert.equal(resolveEmitLegacyTools(undefined, undefined), true);
211
+ delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
212
+ process.env.ENGRAM_EMIT_LEGACY_TOOLS = "false";
213
+ assert.equal(resolveEmitLegacyTools(undefined, undefined), false);
214
+ process.env.ENGRAM_EMIT_LEGACY_TOOLS = "true";
215
+ assert.equal(resolveEmitLegacyTools(undefined, undefined), true);
216
+ } finally {
217
+ if (prevRemnic === undefined) delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
218
+ else process.env.REMNIC_EMIT_LEGACY_TOOLS = prevRemnic;
219
+ if (prevEngram === undefined) delete process.env.ENGRAM_EMIT_LEGACY_TOOLS;
220
+ else process.env.ENGRAM_EMIT_LEGACY_TOOLS = prevEngram;
221
+ }
222
+ });
223
+
224
+ // ============================================================================
225
+ // resolveNamespaceCatalogEnabled
226
+ // ============================================================================
227
+
228
+ test("resolveNamespaceCatalogEnabled: default is true when absent", () => {
229
+ assert.equal(
230
+ resolveNamespaceCatalogEnabled(undefined, undefined),
231
+ true,
232
+ "absent configValue + absent raw → schema default true",
233
+ );
234
+ assert.equal(
235
+ resolveNamespaceCatalogEnabled(undefined, {}),
236
+ true,
237
+ "absent configValue + empty raw → schema default true",
238
+ );
239
+ assert.equal(
240
+ resolveNamespaceCatalogEnabled(true, {}),
241
+ true,
242
+ "merged true (schema default) with empty raw → fall through to default",
243
+ );
244
+ });
245
+
246
+ test("resolveNamespaceCatalogEnabled: merged false (different from schema default) honored as runtime intent", () => {
247
+ // Schema default is true. A merged `false` from the runtime API is
248
+ // operator intent to disable → honor it even though the schema default
249
+ // would be `true`.
250
+ assert.equal(
251
+ resolveNamespaceCatalogEnabled(false, {}),
252
+ false,
253
+ "merged false differs from schema default true — runtime operator intent honored",
254
+ );
255
+ });
256
+
257
+ test("resolveNamespaceCatalogEnabled: runtime (merged) value wins over raw file value", () => {
258
+ assert.equal(
259
+ resolveNamespaceCatalogEnabled(true, { namespaceCatalogEnabled: false }),
260
+ true,
261
+ "merged true wins over raw false (runtime-over-file precedence)",
262
+ );
263
+ assert.equal(
264
+ resolveNamespaceCatalogEnabled(false, { namespaceCatalogEnabled: true }),
265
+ false,
266
+ "merged false wins over raw true (runtime-over-file precedence)",
267
+ );
268
+ });
269
+
270
+ test("resolveNamespaceCatalogEnabled: null raw normalized (round-3 review)", () => {
271
+ // Schema default true. Merged false with null raw differs from default →
272
+ // runtime intent honored.
273
+ assert.equal(
274
+ resolveNamespaceCatalogEnabled(false, null),
275
+ false,
276
+ "null raw with merged false (runtime intent) honored",
277
+ );
278
+ // Merged true (schema default) with null raw → fall through to default.
279
+ assert.equal(
280
+ resolveNamespaceCatalogEnabled(true, null),
281
+ true,
282
+ "null raw with merged true (schema default) preserved",
283
+ );
284
+ });
285
+
286
+ test("resolveNamespaceCatalogEnabled: malformed values throw (rule #51)", () => {
287
+ assert.throws(
288
+ () => resolveNamespaceCatalogEnabled("maybe", {}),
289
+ /namespaceCatalogEnabled must be a boolean-like value/,
290
+ "unrecognized string throws via coerceBooleanLikeOrThrow",
291
+ );
292
+ assert.throws(
293
+ () => resolveNamespaceCatalogEnabled(2, {}),
294
+ /namespaceCatalogEnabled must be a boolean-like value/,
295
+ "unrecognized number throws via coerceBooleanLikeOrThrow",
296
+ );
297
+ });
@@ -0,0 +1,204 @@
1
+ /**
2
+ * emit-legacy-tools — resolution cluster for the `emitLegacyTools` and
3
+ * `namespaceCatalogEnabled` config gates (issues #1427, #1499, #1550).
4
+ *
5
+ * Extracted from packages/remnic-core/src/config.ts (PR #1593, round 6)
6
+ * so the raw-vs-effective split, runtime-override precedence, and
7
+ * fileConfig-null normalization logic specific to these two gates
8
+ * stays out of the god-file `config.ts`. The export surface is exactly
9
+ * the two resolvers plus the `coerceBooleanLikeOrThrow` helper that
10
+ * wraps the local `coerceBooleanLike` — every other config gate in
11
+ * `parseConfig` keeps using its own inline coercion or the
12
+ * `coerceBooleanLike` already exported from
13
+ * `packages/remnic-core/src/connectors/coerce.ts`.
14
+ *
15
+ * Precedence (PR #1593 rounds 1-4, plus the null/loader hardening from
16
+ * rounds 3-5):
17
+ *
18
+ * 1. `configValue` (the first arg) is the MERGED config
19
+ * (runtime-over-file via the
20
+ * `{...fileConfig, ...api.pluginConfig}` spread in src/index.ts).
21
+ * If it's a real boolean, it represents what the operator wants,
22
+ * so honor it. We only fall through when it's the schema-default
23
+ * materialization with no operator authoring in raw.
24
+ * 2. `rawOperatorConfig` (the second arg) is the operator-supplied
25
+ * config block BEFORE the OpenClaw manifest layer applies schema
26
+ * defaults — i.e. the file-backed `loadPluginConfigFromFile` output.
27
+ * When raw has the key with a non-null/undefined value, the file
28
+ * layer authored it. The merged `configValue` reflects the full
29
+ * operator intent (file + runtime), so `configValue` is still
30
+ * authoritative. raw presence is used only as the "operator
31
+ * authored this key" signal — if raw is missing AND configValue
32
+ * equals the schema default, only the schema layer materialized the
33
+ * key (no operator intent anywhere) and we fall through to env /
34
+ * sticky-legacy.
35
+ * 3. Legacy callers (raw undefined): trust configValue as before to
36
+ * preserve the 121+ existing call sites that pass only one arg.
37
+ *
38
+ * Defensive normalization (PR #1593 round 3): JSON null on disk for the
39
+ * operator config block surfaces as `null` in rawOperatorConfig. Both
40
+ * resolvers normalize `null` to `{}` so the `"key" in rawOperatorConfig`
41
+ * check never throws. The file loader
42
+ * (`loadPluginConfigFromFile`) also normalizes null to undefined
43
+ * before reaching here.
44
+ */
45
+
46
+ import { readEnvVar } from "./runtime/env.js";
47
+ import { hasLegacyConnectorEntries } from "./connectors/paths.js";
48
+
49
+ /**
50
+ * Coerce common string/number representations of a boolean to a real
51
+ * boolean. Returns `undefined` when the value cannot be interpreted, so
52
+ * callers can fail fast via `coerceBooleanLikeOrThrow`. Guards against
53
+ * the "string `false` is truthy" footgun (CLAUDE.md gotcha #36) when
54
+ * config values arrive from CLI/env/JSON sources where booleans are
55
+ * sometimes string-typed.
56
+ *
57
+ * Local copy — the canonical implementation lives in
58
+ * `packages/remnic-core/src/connectors/coerce.ts` but is private to that
59
+ * module's `coerceBool` export; duplicating here keeps the god-file
60
+ * contract clean (no cross-module pull) for the two gates that need
61
+ * fail-fast rejection.
62
+ */
63
+ function coerceBooleanLike(value: unknown): boolean | undefined {
64
+ if (typeof value === "boolean") return value;
65
+ if (typeof value === "number") {
66
+ if (value === 1) return true;
67
+ if (value === 0) return false;
68
+ return undefined;
69
+ }
70
+ if (typeof value === "string") {
71
+ const normalized = value.trim().toLowerCase();
72
+ if (
73
+ normalized === "true" ||
74
+ normalized === "1" ||
75
+ normalized === "yes" ||
76
+ normalized === "on"
77
+ ) {
78
+ return true;
79
+ }
80
+ if (
81
+ normalized === "false" ||
82
+ normalized === "0" ||
83
+ normalized === "no" ||
84
+ normalized === "off"
85
+ ) {
86
+ return false;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ /**
93
+ * Coerce a present boolean-like gate value or fail fast. A PRESENT but
94
+ * unrecognized value ("fales", 2) is REJECTED rather than silently
95
+ * defaulting (CLAUDE.md rule #51) — shared by both resolvers below so
96
+ * the rejection behavior cannot drift between them.
97
+ */
98
+ function coerceBooleanLikeOrThrow(label: string, value: unknown): boolean {
99
+ const coerced = coerceBooleanLike(value);
100
+ if (coerced === undefined) {
101
+ throw new Error(
102
+ `${label} must be a boolean-like value (true/false/1/0/yes/no/on/off); got ${JSON.stringify(value)}`,
103
+ );
104
+ }
105
+ return coerced;
106
+ }
107
+
108
+ /**
109
+ * Resolve the `emitLegacyTools` opt-out (issue #1427, defaults revised in
110
+ * #1550). Precedence: operator-set raw config, then merged (post-defaults)
111
+ * config, then the REMNIC_/ENGRAM_ env var, then a sticky-legacy default —
112
+ * `true` only when existing legacy connector entries are present on disk
113
+ * (`hasLegacyConnectorEntries`), `false` for fresh installs.
114
+ */
115
+ export function resolveEmitLegacyTools(
116
+ configValue: unknown,
117
+ rawOperatorConfig: Record<string, unknown> | undefined | null,
118
+ runtimeSet?: ReadonlySet<string>,
119
+ ): boolean {
120
+ // Defensive null normalization — see file header for the rationale.
121
+ if (rawOperatorConfig === null) rawOperatorConfig = {};
122
+ // Schema default for `emitLegacyTools` is `false` (issue #1550).
123
+ const SCHEMA_DEFAULT = false;
124
+ // Round 8: `runtimeAuthored` is now informational only — the resolver
125
+ // does not consult it (see the comment on the check below). Kept as a
126
+ // local so the signature-bound `runtimeSet` parameter remains useful
127
+ // for future refactors and tests can read this state if needed.
128
+ const runtimeAuthored = runtimeSet?.has("emitLegacyTools") ?? false;
129
+ if (rawOperatorConfig !== undefined) {
130
+ if (configValue !== undefined && configValue !== null) {
131
+ const rawValue = (rawOperatorConfig as Record<string, unknown>).emitLegacyTools;
132
+ const rawAuthored =
133
+ "emitLegacyTools" in rawOperatorConfig &&
134
+ rawValue !== null &&
135
+ rawValue !== undefined;
136
+ // Round 8 (PR #1593): revert the runtimeAuthored gate. OpenClaw's
137
+ // `applyDefaults: true` materialization means api.pluginConfig keys
138
+ // can't reliably signal operator authorship; the schema-default
139
+ // comparison alone is the right signal (chatgpt-codex-connector P1
140
+ // on src/index.ts:1348, round 8).
141
+ if (rawAuthored || configValue !== SCHEMA_DEFAULT) {
142
+ return coerceBooleanLikeOrThrow("emitLegacyTools", configValue);
143
+ }
144
+ } else if ("emitLegacyTools" in rawOperatorConfig) {
145
+ const rawValue = (rawOperatorConfig as Record<string, unknown>).emitLegacyTools;
146
+ if (rawValue !== null && rawValue !== undefined) {
147
+ return coerceBooleanLikeOrThrow("emitLegacyTools", rawValue);
148
+ }
149
+ }
150
+ } else if (configValue !== undefined && configValue !== null) {
151
+ // Legacy caller (no rawOperatorConfig) — trust the merged value.
152
+ return coerceBooleanLikeOrThrow("emitLegacyTools", configValue);
153
+ }
154
+ const envRaw =
155
+ readEnvVar("REMNIC_EMIT_LEGACY_TOOLS") ?? readEnvVar("ENGRAM_EMIT_LEGACY_TOOLS");
156
+ if (envRaw !== undefined) {
157
+ return coerceBooleanLikeOrThrow("REMNIC_EMIT_LEGACY_TOOLS", envRaw);
158
+ }
159
+ return hasLegacyConnectorEntries();
160
+ }
161
+
162
+ /**
163
+ * Resolve the `namespaceCatalogEnabled` opt-out (issue #1499). Same
164
+ * raw-vs-effective split as `resolveEmitLegacyTools` — schema-default
165
+ * hardening at the helper level so adding a `false` default later cannot
166
+ * silently flip behavior on upgraded installs (#1550 class hardening).
167
+ */
168
+ export function resolveNamespaceCatalogEnabled(
169
+ configValue: unknown,
170
+ rawOperatorConfig: Record<string, unknown> | undefined | null,
171
+ runtimeSet?: ReadonlySet<string>,
172
+ ): boolean {
173
+ // Defensive null normalization — see file header for the rationale.
174
+ if (rawOperatorConfig === null) rawOperatorConfig = {};
175
+ // Schema default is `true` (the catalog is opt-out).
176
+ const SCHEMA_DEFAULT = true;
177
+ // Round 8: same as the emit variant — `runtimeAuthored` is
178
+ // informational only, the resolver does not consult it.
179
+ const runtimeAuthored = runtimeSet?.has("namespaceCatalogEnabled") ?? false;
180
+ if (rawOperatorConfig !== undefined) {
181
+ if (configValue !== undefined && configValue !== null) {
182
+ const rawValue = (rawOperatorConfig as Record<string, unknown>)
183
+ .namespaceCatalogEnabled;
184
+ const rawAuthored =
185
+ "namespaceCatalogEnabled" in rawOperatorConfig &&
186
+ rawValue !== null &&
187
+ rawValue !== undefined;
188
+ // Round 8: same rollback as resolveEmitLegacyTools.
189
+ if (rawAuthored || configValue !== SCHEMA_DEFAULT) {
190
+ return coerceBooleanLikeOrThrow("namespaceCatalogEnabled", configValue);
191
+ }
192
+ } else if ("namespaceCatalogEnabled" in rawOperatorConfig) {
193
+ const rawValue = (rawOperatorConfig as Record<string, unknown>)
194
+ .namespaceCatalogEnabled;
195
+ if (rawValue !== null && rawValue !== undefined) {
196
+ return coerceBooleanLikeOrThrow("namespaceCatalogEnabled", rawValue);
197
+ }
198
+ }
199
+ } else if (configValue !== undefined && configValue !== null) {
200
+ // Legacy caller (no rawOperatorConfig) — trust the merged value.
201
+ return coerceBooleanLikeOrThrow("namespaceCatalogEnabled", configValue);
202
+ }
203
+ return SCHEMA_DEFAULT;
204
+ }
package/src/index.ts CHANGED
@@ -1311,3 +1311,25 @@ export {
1311
1311
  type ForkCapsuleResult,
1312
1312
  type ForkLineage,
1313
1313
  } from "./transfer/capsule-fork.js";
1314
+
1315
+
1316
+ // ---------------------------------------------------------------------------
1317
+ // Coding-graph engine contract (#1551 PR1) — types owned by core,
1318
+ // implemented by @remnic/coding-graph (à-la-carte optional).
1319
+ // ---------------------------------------------------------------------------
1320
+
1321
+ export {
1322
+ CODING_GRAPH_ENGINE_VERSION,
1323
+ TIER_1_LANGUAGES,
1324
+ } from "./coding/coding-graph-types.js";
1325
+
1326
+ export type {
1327
+ CodingGraphEngine,
1328
+ CodingGraphErrorCode,
1329
+ CodingGraphLanguage,
1330
+ CreateCodingGraphEngineOptions,
1331
+ FileIR,
1332
+ ParseFileInput,
1333
+ ParseResult,
1334
+ SymbolIR,
1335
+ } from "./coding/coding-graph-types.js";