@remnic/core 9.3.655 → 9.3.656

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 (247) hide show
  1. package/dist/access-cli.js +22 -22
  2. package/dist/access-http.d.ts +4 -4
  3. package/dist/access-http.js +10 -10
  4. package/dist/access-mcp.d.ts +4 -4
  5. package/dist/access-mcp.js +9 -9
  6. package/dist/access-schema.d.ts +10 -10
  7. package/dist/{access-service-BEJvriUt.d.ts → access-service-D_nbpexW.d.ts} +33 -2
  8. package/dist/access-service.d.ts +4 -4
  9. package/dist/access-service.js +8 -8
  10. package/dist/action-confidence.d.ts +1 -1
  11. package/dist/active-memory-bridge.d.ts +1 -1
  12. package/dist/active-recall.d.ts +1 -1
  13. package/dist/active-recall.js +1 -1
  14. package/dist/behavior-learner.d.ts +1 -1
  15. package/dist/behavior-signals.d.ts +1 -1
  16. package/dist/bootstrap.d.ts +3 -3
  17. package/dist/briefing.d.ts +1 -1
  18. package/dist/briefing.js +3 -3
  19. package/dist/buffer-surprise-report.d.ts +1 -1
  20. package/dist/buffer.d.ts +1 -1
  21. package/dist/calibration.d.ts +1 -1
  22. package/dist/causal-behavior.d.ts +1 -1
  23. package/dist/causal-consolidation.d.ts +1 -1
  24. package/dist/causal-consolidation.js +4 -4
  25. package/dist/{chunk-PVE7KSQP.js → chunk-2BD7DG37.js} +2 -2
  26. package/dist/{chunk-54LOUIBE.js → chunk-2MXEVL75.js} +2 -2
  27. package/dist/{chunk-55ZMNKMQ.js → chunk-4UL7VPTD.js} +276 -57
  28. package/dist/chunk-4UL7VPTD.js.map +1 -0
  29. package/dist/{chunk-COVZLGMR.js → chunk-54XF2FY7.js} +17 -17
  30. package/dist/{chunk-UYNFWZWG.js → chunk-AGJKWOKV.js} +2 -2
  31. package/dist/{chunk-TDZSSJV4.js → chunk-AZBV4RRY.js} +1 -1
  32. package/dist/chunk-AZBV4RRY.js.map +1 -0
  33. package/dist/{chunk-KOI765XP.js → chunk-CTAV55JM.js} +241 -1
  34. package/dist/chunk-CTAV55JM.js.map +1 -0
  35. package/dist/{chunk-A3Y37UWI.js → chunk-DIBWFCLA.js} +3 -3
  36. package/dist/{chunk-QDVQ4AN2.js → chunk-DR67OK4E.js} +5 -5
  37. package/dist/{chunk-XBIACVCO.js → chunk-EC2AYKRX.js} +2 -2
  38. package/dist/{chunk-IQ53ZSXV.js → chunk-GCYFUTUC.js} +2 -2
  39. package/dist/{chunk-YYN3LIYA.js → chunk-GSHW5VVD.js} +5 -5
  40. package/dist/chunk-GYSYLGNE.js +650 -0
  41. package/dist/chunk-GYSYLGNE.js.map +1 -0
  42. package/dist/{chunk-NRBGRZW4.js → chunk-IOZ5WBWD.js} +2 -2
  43. package/dist/{chunk-NCSJKK23.js → chunk-JSVFEHLL.js} +7 -5
  44. package/dist/chunk-JSVFEHLL.js.map +1 -0
  45. package/dist/{chunk-7LWRCOP7.js → chunk-LZTFCAKE.js} +2 -2
  46. package/dist/{chunk-TEO46GMM.js → chunk-NXCK7DO7.js} +2 -2
  47. package/dist/{chunk-XOFXKASO.js → chunk-PEPHBH2W.js} +2 -2
  48. package/dist/{chunk-WDTUYOLS.js → chunk-QZRKNA5F.js} +2 -2
  49. package/dist/{chunk-PS3SYNHP.js → chunk-R5DB26G6.js} +2 -2
  50. package/dist/{chunk-5QD3QD76.js → chunk-RDW5G6DO.js} +659 -123
  51. package/dist/chunk-RDW5G6DO.js.map +1 -0
  52. package/dist/{chunk-BGKXTVNG.js → chunk-SWDHVH2P.js} +2 -2
  53. package/dist/{chunk-67G4T7KI.js → chunk-SXYCVRLK.js} +3 -3
  54. package/dist/{chunk-UCEABZZN.js → chunk-TFFZUFEP.js} +7 -5
  55. package/dist/chunk-TFFZUFEP.js.map +1 -0
  56. package/dist/{chunk-UCEDY5M7.js → chunk-TIJYQXDI.js} +2 -2
  57. package/dist/{chunk-2RCGZ67B.js → chunk-VAEAGTEQ.js} +3 -3
  58. package/dist/{chunk-XRKQOQLY.js → chunk-WIKMCJUR.js} +2 -2
  59. package/dist/{chunk-KZZ4YAEC.js → chunk-WWMHAMAY.js} +2 -2
  60. package/dist/{chunk-OKW6F5S5.js → chunk-YEZHZCUO.js} +4 -4
  61. package/dist/{chunk-5FOCXX5E.js → chunk-YVVQUAOO.js} +3 -3
  62. package/dist/{chunk-5FOCXX5E.js.map → chunk-YVVQUAOO.js.map} +1 -1
  63. package/dist/{chunk-3XGWCZ63.js → chunk-YXLT4EMM.js} +2 -2
  64. package/dist/{chunk-PTMJ2FH2.js → chunk-Z6UDTNY6.js} +2 -2
  65. package/dist/{cli-BGahB_d3.d.ts → cli-aYxSuPvP.d.ts} +3 -3
  66. package/dist/cli.d.ts +5 -5
  67. package/dist/cli.js +22 -22
  68. package/dist/compounding/engine.d.ts +1 -1
  69. package/dist/compounding/engine.js +3 -3
  70. package/dist/compounding/preference-consolidator.d.ts +1 -1
  71. package/dist/compression-optimizer.d.ts +1 -1
  72. package/dist/config.d.ts +1 -1
  73. package/dist/config.js +1 -1
  74. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  75. package/dist/connectors/codex-materialize-runner.js +3 -3
  76. package/dist/connectors/codex-materialize.d.ts +1 -1
  77. package/dist/connectors/index.d.ts +1 -1
  78. package/dist/connectors/index.js +3 -3
  79. package/dist/consolidation-provenance-check.d.ts +1 -1
  80. package/dist/consolidation-undo.d.ts +1 -1
  81. package/dist/contradiction/index.d.ts +1 -1
  82. package/dist/conversation-index/backend.d.ts +1 -1
  83. package/dist/conversation-index/chunker.d.ts +1 -1
  84. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  85. package/dist/conversation-index/indexer.d.ts +1 -1
  86. package/dist/conversation-index/search.d.ts +1 -1
  87. package/dist/day-summary.d.ts +1 -1
  88. package/dist/delinearize.d.ts +1 -1
  89. package/dist/direct-answer-wiring.d.ts +1 -1
  90. package/dist/direct-answer.d.ts +1 -1
  91. package/dist/embedding-fallback.d.ts +1 -1
  92. package/dist/enrichment/index.d.ts +1 -1
  93. package/dist/entity-retrieval.d.ts +1 -1
  94. package/dist/entity-retrieval.js +3 -3
  95. package/dist/entity-schema.d.ts +1 -1
  96. package/dist/explicit-capture.d.ts +3 -3
  97. package/dist/explicit-cue-recall.js +2 -2
  98. package/dist/extraction-judge-telemetry.d.ts +1 -1
  99. package/dist/extraction-judge-training.d.ts +1 -1
  100. package/dist/extraction-judge.d.ts +1 -1
  101. package/dist/extraction.d.ts +1 -1
  102. package/dist/fallback-llm.d.ts +1 -1
  103. package/dist/focused-list-recall.js +2 -2
  104. package/dist/identity-continuity.d.ts +1 -1
  105. package/dist/importance.d.ts +1 -1
  106. package/dist/index.d.ts +121 -121
  107. package/dist/index.js +32 -32
  108. package/dist/intent.d.ts +1 -1
  109. package/dist/lcm/engine.d.ts +1 -1
  110. package/dist/lcm/index.d.ts +1 -1
  111. package/dist/lcm/tools.d.ts +1 -1
  112. package/dist/lcm-fallback-read.js +1 -1
  113. package/dist/lifecycle.d.ts +1 -1
  114. package/dist/live-connectors-runner.d.ts +1 -1
  115. package/dist/local-llm.d.ts +1 -1
  116. package/dist/maintenance/memory-governance.d.ts +1 -1
  117. package/dist/maintenance/memory-governance.js +3 -3
  118. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  119. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  120. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  121. package/dist/memory-action-policy.d.ts +1 -1
  122. package/dist/memory-cache.d.ts +1 -1
  123. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  124. package/dist/memory-projection-store.d.ts +1 -1
  125. package/dist/memory-provenance.d.ts +1 -1
  126. package/dist/memory-worth-outcomes.d.ts +1 -1
  127. package/dist/models-json.d.ts +1 -1
  128. package/dist/namespaces/migrate.d.ts +1 -1
  129. package/dist/namespaces/migrate.js +4 -4
  130. package/dist/namespaces/principal.d.ts +1 -1
  131. package/dist/namespaces/search.d.ts +1 -1
  132. package/dist/namespaces/storage.d.ts +1 -1
  133. package/dist/namespaces/storage.js +3 -3
  134. package/dist/native-knowledge.d.ts +1 -1
  135. package/dist/operator-toolkit.d.ts +1 -1
  136. package/dist/operator-toolkit.js +7 -7
  137. package/dist/{orchestrator-BgzZlWxH.d.ts → orchestrator-D1wcmPNj.d.ts} +8 -2
  138. package/dist/orchestrator.d.ts +3 -3
  139. package/dist/orchestrator.js +18 -18
  140. package/dist/patterns-cli.d.ts +1 -1
  141. package/dist/policy-runtime.d.ts +1 -1
  142. package/dist/qmd-recall-cache.d.ts +1 -1
  143. package/dist/qmd.d.ts +1 -1
  144. package/dist/recall-disclosure-escalation.d.ts +1 -1
  145. package/dist/recall-explain-renderer.d.ts +1 -1
  146. package/dist/recall-explain-renderer.js +3 -3
  147. package/dist/recall-planner-llm.d.ts +1 -1
  148. package/dist/recall-state.d.ts +1 -1
  149. package/dist/recall-tag-filter.d.ts +1 -1
  150. package/dist/recall-xray-cli.d.ts +1 -1
  151. package/dist/recall-xray-cli.js +4 -4
  152. package/dist/recall-xray-renderer.d.ts +1 -1
  153. package/dist/recall-xray-renderer.js +3 -3
  154. package/dist/recall-xray.d.ts +1 -1
  155. package/dist/recall-xray.js +2 -2
  156. package/dist/resolve-auth-token.d.ts +1 -1
  157. package/dist/response-guidance-recall.js +2 -2
  158. package/dist/resume-bundles.js +2 -2
  159. package/dist/retrieval-agents.d.ts +1 -1
  160. package/dist/retrieval-tiers.d.ts +1 -1
  161. package/dist/routing/engine.d.ts +1 -1
  162. package/dist/routing/store.d.ts +1 -1
  163. package/dist/search/embed-helper.d.ts +1 -1
  164. package/dist/search/factory.d.ts +1 -1
  165. package/dist/search/index.d.ts +1 -1
  166. package/dist/search/lancedb-backend.d.ts +1 -1
  167. package/dist/search/meilisearch-backend.d.ts +1 -1
  168. package/dist/search/noop-backend.d.ts +1 -1
  169. package/dist/search/orama-backend.d.ts +1 -1
  170. package/dist/search/port.d.ts +1 -1
  171. package/dist/search/remote-backend.d.ts +1 -1
  172. package/dist/{semantic-consolidation-Z8d_uMq8.d.ts → semantic-consolidation-MWOdNtSE.d.ts} +1 -1
  173. package/dist/semantic-consolidation.d.ts +2 -2
  174. package/dist/semantic-consolidation.js +4 -4
  175. package/dist/semantic-rule-promotion.js +3 -3
  176. package/dist/semantic-rule-verifier.d.ts +3 -2
  177. package/dist/semantic-rule-verifier.js +5 -3
  178. package/dist/session-observer-bands.d.ts +1 -1
  179. package/dist/session-observer-state.d.ts +1 -1
  180. package/dist/shared-context/manager.d.ts +1 -1
  181. package/dist/signal.d.ts +1 -1
  182. package/dist/storage.d.ts +1 -1
  183. package/dist/storage.js +2 -2
  184. package/dist/summarizer.d.ts +1 -1
  185. package/dist/summary-snapshot.d.ts +1 -1
  186. package/dist/targeted-fact-recall.js +2 -2
  187. package/dist/temporal-supersession.d.ts +1 -1
  188. package/dist/temporal-validity.d.ts +1 -1
  189. package/dist/threading.d.ts +1 -1
  190. package/dist/tier-migration.d.ts +1 -1
  191. package/dist/tier-routing.d.ts +1 -1
  192. package/dist/topics.d.ts +1 -1
  193. package/dist/transcript.d.ts +1 -1
  194. package/dist/{types-2OPlQWJG.d.ts → types-CgcCpUrf.d.ts} +39 -1
  195. package/dist/types.d.ts +1 -1
  196. package/dist/types.js +1 -1
  197. package/dist/utility-runtime.d.ts +1 -1
  198. package/dist/verified-recall.d.ts +2 -1
  199. package/dist/verified-recall.js +5 -3
  200. package/package.json +1 -1
  201. package/src/access-service-observe-lcm-parity.test.ts +86 -1
  202. package/src/access-service-observe-scope.test.ts +283 -1
  203. package/src/access-service-raw-excerpt-read-gate.test.ts +53 -0
  204. package/src/access-service.ts +391 -93
  205. package/src/coding/coding-namespace.ts +0 -3
  206. package/src/config.ts +282 -0
  207. package/src/lcm-fallback-read.ts +2 -6
  208. package/src/namespaces/scope-profiles.test.ts +1074 -0
  209. package/src/namespaces/scope-profiles.ts +456 -0
  210. package/src/orchestrator-flush.test.ts +142 -0
  211. package/src/orchestrator-source-attribution.test.ts +73 -0
  212. package/src/orchestrator.ts +835 -163
  213. package/src/semantic-rule-verifier.ts +13 -6
  214. package/src/types.ts +52 -0
  215. package/src/verified-recall.ts +10 -6
  216. package/dist/chunk-55ZMNKMQ.js.map +0 -1
  217. package/dist/chunk-5QD3QD76.js.map +0 -1
  218. package/dist/chunk-KOI765XP.js.map +0 -1
  219. package/dist/chunk-MMJANTJX.js +0 -339
  220. package/dist/chunk-MMJANTJX.js.map +0 -1
  221. package/dist/chunk-NCSJKK23.js.map +0 -1
  222. package/dist/chunk-TDZSSJV4.js.map +0 -1
  223. package/dist/chunk-UCEABZZN.js.map +0 -1
  224. /package/dist/{chunk-PVE7KSQP.js.map → chunk-2BD7DG37.js.map} +0 -0
  225. /package/dist/{chunk-54LOUIBE.js.map → chunk-2MXEVL75.js.map} +0 -0
  226. /package/dist/{chunk-COVZLGMR.js.map → chunk-54XF2FY7.js.map} +0 -0
  227. /package/dist/{chunk-UYNFWZWG.js.map → chunk-AGJKWOKV.js.map} +0 -0
  228. /package/dist/{chunk-A3Y37UWI.js.map → chunk-DIBWFCLA.js.map} +0 -0
  229. /package/dist/{chunk-QDVQ4AN2.js.map → chunk-DR67OK4E.js.map} +0 -0
  230. /package/dist/{chunk-XBIACVCO.js.map → chunk-EC2AYKRX.js.map} +0 -0
  231. /package/dist/{chunk-IQ53ZSXV.js.map → chunk-GCYFUTUC.js.map} +0 -0
  232. /package/dist/{chunk-YYN3LIYA.js.map → chunk-GSHW5VVD.js.map} +0 -0
  233. /package/dist/{chunk-NRBGRZW4.js.map → chunk-IOZ5WBWD.js.map} +0 -0
  234. /package/dist/{chunk-7LWRCOP7.js.map → chunk-LZTFCAKE.js.map} +0 -0
  235. /package/dist/{chunk-TEO46GMM.js.map → chunk-NXCK7DO7.js.map} +0 -0
  236. /package/dist/{chunk-XOFXKASO.js.map → chunk-PEPHBH2W.js.map} +0 -0
  237. /package/dist/{chunk-WDTUYOLS.js.map → chunk-QZRKNA5F.js.map} +0 -0
  238. /package/dist/{chunk-PS3SYNHP.js.map → chunk-R5DB26G6.js.map} +0 -0
  239. /package/dist/{chunk-BGKXTVNG.js.map → chunk-SWDHVH2P.js.map} +0 -0
  240. /package/dist/{chunk-67G4T7KI.js.map → chunk-SXYCVRLK.js.map} +0 -0
  241. /package/dist/{chunk-UCEDY5M7.js.map → chunk-TIJYQXDI.js.map} +0 -0
  242. /package/dist/{chunk-2RCGZ67B.js.map → chunk-VAEAGTEQ.js.map} +0 -0
  243. /package/dist/{chunk-XRKQOQLY.js.map → chunk-WIKMCJUR.js.map} +0 -0
  244. /package/dist/{chunk-KZZ4YAEC.js.map → chunk-WWMHAMAY.js.map} +0 -0
  245. /package/dist/{chunk-OKW6F5S5.js.map → chunk-YEZHZCUO.js.map} +0 -0
  246. /package/dist/{chunk-3XGWCZ63.js.map → chunk-YXLT4EMM.js.map} +0 -0
  247. /package/dist/{chunk-PTMJ2FH2.js.map → chunk-Z6UDTNY6.js.map} +0 -0
@@ -0,0 +1,1074 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { combineNamespaces, resolveCodingNamespaceOverlay } from "../coding/coding-namespace.js";
5
+ import { stableHash } from "../coding/git-context.js";
6
+ import { parseConfig } from "../config.js";
7
+ import type { CodingContext } from "../types.js";
8
+ import {
9
+ expandScopeProfileReadNamespaces,
10
+ resolveScopeProfilePlan,
11
+ } from "./scope-profiles.js";
12
+
13
+ function teamCodingConfig() {
14
+ return parseConfig({
15
+ namespacesEnabled: true,
16
+ defaultNamespace: "default",
17
+ sharedNamespace: "shared",
18
+ namespacePolicies: [
19
+ {
20
+ name: "pi-geek",
21
+ readPrincipals: ["pi-geek"],
22
+ writePrincipals: ["pi-geek"],
23
+ },
24
+ {
25
+ name: "pi-friend",
26
+ readPrincipals: ["pi-friend"],
27
+ writePrincipals: ["pi-friend"],
28
+ },
29
+ {
30
+ name: "shared",
31
+ readPrincipals: ["pi-geek", "pi-friend"],
32
+ writePrincipals: ["pi-geek", "pi-friend"],
33
+ },
34
+ ],
35
+ scopeProfiles: {
36
+ teamCoding: {
37
+ readOrder: ["userProject", "teamProject", "userGlobal", "serverShared"],
38
+ writeDefault: "userProject",
39
+ promotionTargets: ["teamProject", "serverShared"],
40
+ teamProject: {
41
+ namespaceTemplate: "team-{teamId}-project-{projectHash}",
42
+ },
43
+ },
44
+ },
45
+ defaultScopeProfile: "teamCoding",
46
+ teams: {
47
+ pi: {
48
+ principals: ["pi-geek", "pi-friend"],
49
+ read: ["pi-geek", "pi-friend"],
50
+ write: ["pi-geek", "pi-friend"],
51
+ promote: ["pi-geek", "pi-friend"],
52
+ },
53
+ },
54
+ });
55
+ }
56
+
57
+ const codingContext: CodingContext = {
58
+ projectId: "tag:remnic",
59
+ branch: null,
60
+ rootPath: "tag:remnic",
61
+ defaultBranch: null,
62
+ };
63
+
64
+ test("scope profile absent preserves legacy caller behavior by resolving to null", () => {
65
+ const config = parseConfig({
66
+ namespacesEnabled: true,
67
+ defaultNamespace: "default",
68
+ sharedNamespace: "shared",
69
+ });
70
+ assert.equal(
71
+ resolveScopeProfilePlan({
72
+ config,
73
+ principal: "pi-geek",
74
+ codingContext,
75
+ codingOverlay: resolveCodingNamespaceOverlay(
76
+ codingContext,
77
+ config.codingMode,
78
+ config.defaultNamespace,
79
+ ),
80
+ }),
81
+ null,
82
+ );
83
+ });
84
+
85
+ test("teamCoding profile resolves user-project, team-project, user-global, and shared layers in order", () => {
86
+ const config = teamCodingConfig();
87
+ const overlay = resolveCodingNamespaceOverlay(
88
+ codingContext,
89
+ config.codingMode,
90
+ config.defaultNamespace,
91
+ );
92
+ assert.ok(overlay);
93
+
94
+ const geek = resolveScopeProfilePlan({
95
+ config,
96
+ principal: "pi-geek",
97
+ codingContext,
98
+ codingOverlay: overlay,
99
+ });
100
+ const friend = resolveScopeProfilePlan({
101
+ config,
102
+ principal: "pi-friend",
103
+ codingContext,
104
+ codingOverlay: overlay,
105
+ });
106
+
107
+ assert.ok(geek);
108
+ assert.ok(friend);
109
+ const geekProject = combineNamespaces("pi-geek", overlay.namespace);
110
+ const friendProject = combineNamespaces("pi-friend", overlay.namespace);
111
+ assert.equal(geek.writeNamespace, geekProject);
112
+ assert.equal(friend.writeNamespace, friendProject);
113
+ assert.notEqual(geekProject, friendProject);
114
+ assert.deepEqual(geek.readNamespaces, [
115
+ geekProject,
116
+ "team-pi-project-2d7ea3c1",
117
+ "pi-geek",
118
+ "shared",
119
+ ]);
120
+ assert.deepEqual(friend.readNamespaces, [
121
+ friendProject,
122
+ "team-pi-project-2d7ea3c1",
123
+ "pi-friend",
124
+ "shared",
125
+ ]);
126
+ assert.ok(!geek.readNamespaces.includes(friendProject));
127
+ assert.ok(!friend.readNamespaces.includes(geekProject));
128
+ assert.deepEqual(
129
+ geek.promotionTargets.map((target) => [target.target, target.namespace, target.authorized]),
130
+ [
131
+ ["teamProject", "team-pi-project-2d7ea3c1", true],
132
+ ["serverShared", "shared", true],
133
+ ],
134
+ );
135
+ });
136
+
137
+ test("explicit user-project namespace policies override base self access", () => {
138
+ const baseConfig = teamCodingConfig();
139
+ const overlay = resolveCodingNamespaceOverlay(
140
+ codingContext,
141
+ baseConfig.codingMode,
142
+ baseConfig.defaultNamespace,
143
+ );
144
+ assert.ok(overlay);
145
+ const deniedProject = combineNamespaces("pi-geek", overlay.namespace);
146
+ const config = parseConfig({
147
+ namespacesEnabled: true,
148
+ defaultNamespace: "default",
149
+ sharedNamespace: "shared",
150
+ namespacePolicies: [
151
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
152
+ { name: deniedProject, readPrincipals: [], writePrincipals: [] },
153
+ ],
154
+ scopeProfiles: {
155
+ projectOnly: {
156
+ readOrder: ["userProject", "userGlobal"],
157
+ writeDefault: "userProject",
158
+ },
159
+ },
160
+ defaultScopeProfile: "projectOnly",
161
+ });
162
+
163
+ const plan = resolveScopeProfilePlan({
164
+ config,
165
+ principal: "pi-geek",
166
+ codingContext,
167
+ codingOverlay: overlay,
168
+ });
169
+
170
+ assert.ok(plan);
171
+ assert.equal(plan.writeLayer, "userGlobal");
172
+ assert.equal(plan.writeNamespace, "pi-geek");
173
+ assert.deepEqual(plan.readNamespaces, ["pi-geek"]);
174
+ const projectLayer = plan.layers.find((layer) => layer.id === "userProject");
175
+ assert.equal(projectLayer?.namespace, deniedProject);
176
+ assert.equal(projectLayer?.readable, false);
177
+ assert.equal(projectLayer?.writable, false);
178
+ });
179
+
180
+ test("userProject profile namespaces stay principal-specific without namespace policies", () => {
181
+ const config = parseConfig({
182
+ namespacesEnabled: true,
183
+ defaultNamespace: "default",
184
+ sharedNamespace: "shared",
185
+ codingMode: { projectScope: true },
186
+ scopeProfiles: {
187
+ hosted: {
188
+ readOrder: ["userProject"],
189
+ writeDefault: "userProject",
190
+ },
191
+ },
192
+ defaultScopeProfile: "hosted",
193
+ });
194
+ const overlay = resolveCodingNamespaceOverlay(codingContext, config.codingMode, config.defaultNamespace);
195
+ assert.ok(overlay);
196
+
197
+ const geek = resolveScopeProfilePlan({ config, principal: "pi-geek", codingContext, codingOverlay: overlay });
198
+ const friend = resolveScopeProfilePlan({ config, principal: "pi-friend", codingContext, codingOverlay: overlay });
199
+
200
+ assert.ok(geek);
201
+ assert.ok(friend);
202
+ assert.equal(geek.writeNamespace, combineNamespaces("pi-geek", overlay.namespace));
203
+ assert.equal(friend.writeNamespace, combineNamespaces("pi-friend", overlay.namespace));
204
+ assert.notEqual(geek.writeNamespace, friend.writeNamespace);
205
+ });
206
+
207
+ test("scope profile effective reads retain coding fallbacks without omitted legacy namespaces", () => {
208
+ const config = parseConfig({
209
+ namespacesEnabled: true,
210
+ defaultNamespace: "default",
211
+ sharedNamespace: "shared",
212
+ defaultRecallNamespaces: ["self", "shared"],
213
+ codingMode: { projectScope: true, branchScope: true },
214
+ namespacePolicies: [
215
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
216
+ { name: "shared", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
217
+ {
218
+ name: "team-extra",
219
+ readPrincipals: ["pi-geek"],
220
+ writePrincipals: [],
221
+ includeInRecallByDefault: true,
222
+ },
223
+ ],
224
+ scopeProfiles: {
225
+ teamCoding: {
226
+ readOrder: ["userProject", "teamProject"],
227
+ writeDefault: "userProject",
228
+ teamProject: {
229
+ namespaceTemplate: "team-{teamId}-project-{projectHash}",
230
+ },
231
+ },
232
+ },
233
+ defaultScopeProfile: "teamCoding",
234
+ teams: {
235
+ pi: {
236
+ principals: ["pi-geek"],
237
+ read: ["pi-geek"],
238
+ write: ["pi-geek"],
239
+ promote: ["pi-geek"],
240
+ },
241
+ },
242
+ });
243
+ const branchContext: CodingContext = {
244
+ projectId: "origin:aaaa0000",
245
+ branch: "feat/x",
246
+ rootPath: "origin:aaaa0000",
247
+ defaultBranch: "main",
248
+ };
249
+ const overlay = resolveCodingNamespaceOverlay(
250
+ branchContext,
251
+ config.codingMode,
252
+ config.defaultNamespace,
253
+ );
254
+ assert.ok(overlay);
255
+ const plan = resolveScopeProfilePlan({
256
+ config,
257
+ principal: "pi-geek",
258
+ codingContext: branchContext,
259
+ codingOverlay: overlay,
260
+ });
261
+ assert.ok(plan);
262
+ const teamProjectNamespace = `team-pi-project-${stableHash(branchContext.projectId)}`;
263
+
264
+ assert.deepEqual(plan.readNamespaces, [
265
+ combineNamespaces("pi-geek", overlay.namespace),
266
+ teamProjectNamespace,
267
+ ]);
268
+ assert.deepEqual(
269
+ expandScopeProfileReadNamespaces({
270
+ profilePlan: plan,
271
+ principalSelfNamespace: "pi-geek",
272
+ config,
273
+ principal: "pi-geek",
274
+ codingOverlay: overlay,
275
+ legacyRecallNamespaces: ["pi-geek", "shared", "team-extra"],
276
+ }),
277
+ [
278
+ combineNamespaces("pi-geek", overlay.namespace),
279
+ teamProjectNamespace,
280
+ combineNamespaces("pi-geek", overlay.readFallbacks[0]!),
281
+ ],
282
+ );
283
+ });
284
+
285
+ test("scope profile expansion respects explicit policies on user-project fallbacks", () => {
286
+ const branchContext: CodingContext = {
287
+ projectId: "origin:aaaa0000",
288
+ branch: "feat/x",
289
+ rootPath: "origin:aaaa0000",
290
+ defaultBranch: "main",
291
+ };
292
+ const initialConfig = parseConfig({
293
+ defaultNamespace: "default",
294
+ sharedNamespace: "shared",
295
+ codingMode: { projectScope: true, branchScope: true },
296
+ });
297
+ const overlay = resolveCodingNamespaceOverlay(branchContext, initialConfig.codingMode, initialConfig.defaultNamespace);
298
+ assert.ok(overlay);
299
+ const deniedProjectFallback = combineNamespaces("pi-geek", overlay.readFallbacks[0]!);
300
+ const config = parseConfig({
301
+ namespacesEnabled: true,
302
+ defaultNamespace: "default",
303
+ sharedNamespace: "shared",
304
+ codingMode: { projectScope: true, branchScope: true },
305
+ namespacePolicies: [
306
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
307
+ { name: deniedProjectFallback, readPrincipals: [], writePrincipals: [] },
308
+ ],
309
+ scopeProfiles: {
310
+ projectOnly: {
311
+ readOrder: ["userProject"],
312
+ writeDefault: "userProject",
313
+ },
314
+ },
315
+ defaultScopeProfile: "projectOnly",
316
+ });
317
+ const plan = resolveScopeProfilePlan({
318
+ config,
319
+ principal: "pi-geek",
320
+ codingContext: branchContext,
321
+ codingOverlay: overlay,
322
+ });
323
+ assert.ok(plan);
324
+
325
+ const expanded = expandScopeProfileReadNamespaces({
326
+ profilePlan: plan,
327
+ principalSelfNamespace: "pi-geek",
328
+ config,
329
+ principal: "pi-geek",
330
+ codingOverlay: overlay,
331
+ legacyRecallNamespaces: ["pi-geek"],
332
+ });
333
+
334
+ assert.ok(!expanded.includes(deniedProjectFallback));
335
+ });
336
+
337
+ test("scope profile expansion does not add user-project fallbacks when userProject is omitted from readOrder", () => {
338
+ const config = parseConfig({
339
+ namespacesEnabled: true,
340
+ defaultNamespace: "default",
341
+ sharedNamespace: "shared",
342
+ defaultRecallNamespaces: ["self", "shared"],
343
+ codingMode: { projectScope: true, branchScope: true },
344
+ namespacePolicies: [
345
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
346
+ ],
347
+ scopeProfiles: {
348
+ teamOnly: {
349
+ readOrder: ["teamProject"],
350
+ writeDefault: "userProject",
351
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projectHash}" },
352
+ },
353
+ },
354
+ defaultScopeProfile: "teamOnly",
355
+ teams: {
356
+ pi: {
357
+ principals: ["pi-geek"],
358
+ read: ["pi-geek"],
359
+ write: ["pi-geek"],
360
+ promote: ["pi-geek"],
361
+ },
362
+ },
363
+ });
364
+ const branchContext: CodingContext = {
365
+ projectId: "origin:aaaa0000",
366
+ branch: "feat/x",
367
+ rootPath: "origin:aaaa0000",
368
+ defaultBranch: "main",
369
+ };
370
+ const overlay = resolveCodingNamespaceOverlay(branchContext, config.codingMode, config.defaultNamespace);
371
+ assert.ok(overlay);
372
+ const plan = resolveScopeProfilePlan({
373
+ config,
374
+ principal: "pi-geek",
375
+ codingContext: branchContext,
376
+ codingOverlay: overlay,
377
+ });
378
+ assert.ok(plan);
379
+ const teamProjectNamespace = `team-pi-project-${stableHash(branchContext.projectId)}`;
380
+
381
+ assert.deepEqual(
382
+ expandScopeProfileReadNamespaces({
383
+ profilePlan: plan,
384
+ principalSelfNamespace: "pi-geek",
385
+ config,
386
+ principal: "pi-geek",
387
+ codingOverlay: overlay,
388
+ legacyRecallNamespaces: ["pi-geek", "shared"],
389
+ }),
390
+ [teamProjectNamespace],
391
+ );
392
+ });
393
+
394
+ test("scope profile expansion does not add global fallback when userGlobal is omitted from readOrder", () => {
395
+ const config = parseConfig({
396
+ namespacesEnabled: true,
397
+ defaultNamespace: "default",
398
+ sharedNamespace: "shared",
399
+ codingMode: { projectScope: true, branchScope: true, globalFallback: true },
400
+ namespacePolicies: [
401
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
402
+ ],
403
+ scopeProfiles: {
404
+ projectOnly: {
405
+ readOrder: ["userProject"],
406
+ writeDefault: "userProject",
407
+ },
408
+ },
409
+ defaultScopeProfile: "projectOnly",
410
+ });
411
+ const branchContext: CodingContext = {
412
+ projectId: "origin:aaaa0000",
413
+ branch: "feat/x",
414
+ rootPath: "origin:aaaa0000",
415
+ defaultBranch: "main",
416
+ };
417
+ const overlay = resolveCodingNamespaceOverlay(branchContext, config.codingMode, config.defaultNamespace);
418
+ assert.ok(overlay);
419
+ const plan = resolveScopeProfilePlan({
420
+ config,
421
+ principal: "pi-geek",
422
+ codingContext: branchContext,
423
+ codingOverlay: overlay,
424
+ });
425
+ assert.ok(plan);
426
+
427
+ const expanded = expandScopeProfileReadNamespaces({
428
+ profilePlan: plan,
429
+ principalSelfNamespace: "pi-geek",
430
+ config,
431
+ principal: "pi-geek",
432
+ codingOverlay: overlay,
433
+ legacyRecallNamespaces: ["pi-geek", "shared"],
434
+ });
435
+
436
+ assert.deepEqual(expanded, [
437
+ combineNamespaces("pi-geek", overlay.namespace),
438
+ combineNamespaces("pi-geek", overlay.readFallbacks[0]!),
439
+ ]);
440
+ assert.ok(!expanded.includes("pi-geek"));
441
+ });
442
+
443
+ test("scope profile prefers writable implicit team mapping when teamProject is the write layer", () => {
444
+ const config = parseConfig({
445
+ namespacesEnabled: true,
446
+ defaultNamespace: "default",
447
+ sharedNamespace: "shared",
448
+ namespacePolicies: [
449
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
450
+ ],
451
+ scopeProfiles: {
452
+ hosted: {
453
+ readOrder: ["teamProject"],
454
+ writeDefault: "teamProject",
455
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projectHash}" },
456
+ },
457
+ },
458
+ defaultScopeProfile: "hosted",
459
+ teams: {
460
+ ops: {
461
+ principals: ["alice"],
462
+ read: ["alice"],
463
+ write: [],
464
+ promote: [],
465
+ },
466
+ core: {
467
+ principals: ["alice"],
468
+ read: ["alice"],
469
+ write: ["alice"],
470
+ promote: ["alice"],
471
+ },
472
+ },
473
+ });
474
+ const overlay = resolveCodingNamespaceOverlay(codingContext, config.codingMode, config.defaultNamespace);
475
+ assert.ok(overlay);
476
+
477
+ const plan = resolveScopeProfilePlan({
478
+ config,
479
+ principal: "alice",
480
+ codingContext,
481
+ codingOverlay: overlay,
482
+ });
483
+
484
+ assert.ok(plan);
485
+ assert.equal(plan.writeNamespace, "team-core-project-2d7ea3c1");
486
+ assert.deepEqual(plan.readNamespaces, ["team-core-project-2d7ea3c1"]);
487
+ });
488
+
489
+ test("scope profile chooses a readable implicit team mapping before promote-only memberships", () => {
490
+ const config = parseConfig({
491
+ namespacesEnabled: true,
492
+ defaultNamespace: "default",
493
+ sharedNamespace: "shared",
494
+ namespacePolicies: [
495
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
496
+ ],
497
+ scopeProfiles: {
498
+ hosted: {
499
+ readOrder: ["teamProject"],
500
+ writeDefault: "teamProject",
501
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projectHash}" },
502
+ },
503
+ },
504
+ defaultScopeProfile: "hosted",
505
+ teams: {
506
+ ops: {
507
+ principals: [],
508
+ read: [],
509
+ write: [],
510
+ promote: ["alice"],
511
+ },
512
+ core: {
513
+ principals: ["alice"],
514
+ read: ["alice"],
515
+ write: ["alice"],
516
+ promote: ["alice"],
517
+ },
518
+ },
519
+ });
520
+ const overlay = resolveCodingNamespaceOverlay(codingContext, config.codingMode, config.defaultNamespace);
521
+ assert.ok(overlay);
522
+
523
+ const plan = resolveScopeProfilePlan({
524
+ config,
525
+ principal: "alice",
526
+ codingContext,
527
+ codingOverlay: overlay,
528
+ });
529
+
530
+ assert.ok(plan);
531
+ assert.equal(plan.writeNamespace, "team-core-project-2d7ea3c1");
532
+ assert.deepEqual(plan.readNamespaces, ["team-core-project-2d7ea3c1"]);
533
+ });
534
+
535
+ test("scope profile prefers promotable implicit team mapping for promotion-only team targets", () => {
536
+ const config = parseConfig({
537
+ namespacesEnabled: true,
538
+ defaultNamespace: "default",
539
+ sharedNamespace: "shared",
540
+ namespacePolicies: [
541
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
542
+ ],
543
+ scopeProfiles: {
544
+ hosted: {
545
+ readOrder: ["userGlobal"],
546
+ writeDefault: "userGlobal",
547
+ promotionTargets: ["teamProject"],
548
+ autoPromote: { enabled: true, targets: ["teamProject"] },
549
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projectHash}" },
550
+ },
551
+ },
552
+ defaultScopeProfile: "hosted",
553
+ teams: {
554
+ ops: {
555
+ principals: ["alice"],
556
+ read: ["alice"],
557
+ write: [],
558
+ promote: [],
559
+ },
560
+ core: {
561
+ principals: ["alice"],
562
+ read: ["alice"],
563
+ write: [],
564
+ promote: ["alice"],
565
+ },
566
+ },
567
+ });
568
+ const overlay = resolveCodingNamespaceOverlay(codingContext, config.codingMode, config.defaultNamespace);
569
+ assert.ok(overlay);
570
+
571
+ const plan = resolveScopeProfilePlan({
572
+ config,
573
+ principal: "alice",
574
+ codingContext,
575
+ codingOverlay: overlay,
576
+ });
577
+
578
+ assert.ok(plan);
579
+ assert.equal(plan.writeNamespace, "alice");
580
+ assert.deepEqual(plan.readNamespaces, ["alice"]);
581
+ assert.deepEqual(
582
+ plan.promotionTargets.map((target) => [target.target, target.namespace, target.authorized]),
583
+ [["teamProject", "team-core-project-2d7ea3c1", true]],
584
+ );
585
+ });
586
+
587
+ test("scope profile denies unauthorized team-project promotion while preserving readable layers", () => {
588
+ const config = parseConfig({
589
+ namespacesEnabled: true,
590
+ defaultNamespace: "default",
591
+ sharedNamespace: "shared",
592
+ namespacePolicies: [
593
+ {
594
+ name: "pi-observer",
595
+ readPrincipals: ["pi-observer"],
596
+ writePrincipals: ["pi-observer"],
597
+ },
598
+ {
599
+ name: "shared",
600
+ readPrincipals: ["pi-observer"],
601
+ writePrincipals: ["pi-maintainer"],
602
+ },
603
+ ],
604
+ scopeProfiles: {
605
+ teamCoding: {
606
+ readOrder: ["teamProject", "userGlobal", "serverShared"],
607
+ writeDefault: "userGlobal",
608
+ promotionTargets: ["teamProject", "serverShared"],
609
+ teamProject: {
610
+ namespaceTemplate: "team-{teamId}-project-{projectHash}",
611
+ },
612
+ },
613
+ },
614
+ defaultScopeProfile: "teamCoding",
615
+ teams: {
616
+ pi: {
617
+ principals: ["pi-observer"],
618
+ read: ["pi-observer"],
619
+ write: [],
620
+ promote: [],
621
+ },
622
+ },
623
+ });
624
+ const overlay = resolveCodingNamespaceOverlay(
625
+ codingContext,
626
+ config.codingMode,
627
+ config.defaultNamespace,
628
+ );
629
+ assert.ok(overlay);
630
+
631
+ const plan = resolveScopeProfilePlan({
632
+ config,
633
+ principal: "pi-observer",
634
+ codingContext,
635
+ codingOverlay: overlay,
636
+ });
637
+
638
+ assert.ok(plan);
639
+ assert.deepEqual(plan.readNamespaces, ["team-pi-project-2d7ea3c1", "pi-observer", "shared"]);
640
+ assert.deepEqual(
641
+ plan.promotionTargets.map((target) => [target.target, target.namespace, target.authorized]),
642
+ [
643
+ ["teamProject", "team-pi-project-2d7ea3c1", false],
644
+ ["serverShared", "shared", false],
645
+ ],
646
+ );
647
+ });
648
+
649
+ test("scope profile missing project context falls back without inventing project namespaces", () => {
650
+ const config = teamCodingConfig();
651
+ const plan = resolveScopeProfilePlan({
652
+ config,
653
+ principal: "pi-geek",
654
+ codingContext: null,
655
+ codingOverlay: null,
656
+ });
657
+
658
+ assert.ok(plan);
659
+ assert.equal(plan.writeNamespace, "pi-geek");
660
+ assert.equal(plan.writeLayer, "userGlobal");
661
+ assert.deepEqual(plan.readNamespaces, ["pi-geek", "shared"]);
662
+ assert.ok(plan.warnings.some((warning) => warning.includes("writeDefault userProject unavailable")));
663
+ });
664
+
665
+ test("scope profile unavailable write default uses next readable writable layer", () => {
666
+ const config = parseConfig({
667
+ namespacesEnabled: true,
668
+ defaultNamespace: "default",
669
+ sharedNamespace: "shared",
670
+ namespacePolicies: [
671
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: [] },
672
+ { name: "shared", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
673
+ ],
674
+ scopeProfiles: {
675
+ teamCoding: {
676
+ readOrder: ["userProject", "serverShared"],
677
+ writeDefault: "userProject",
678
+ },
679
+ },
680
+ defaultScopeProfile: "teamCoding",
681
+ });
682
+
683
+ const plan = resolveScopeProfilePlan({
684
+ config,
685
+ principal: "pi-geek",
686
+ codingContext: null,
687
+ codingOverlay: null,
688
+ });
689
+
690
+ assert.ok(plan);
691
+ assert.equal(plan.writeLayer, "serverShared");
692
+ assert.equal(plan.writeNamespace, "shared");
693
+ assert.ok(plan.warnings.some((warning) => warning.includes("writeDefault userProject unavailable")));
694
+ assert.deepEqual(plan.readNamespaces, ["shared"]);
695
+ });
696
+
697
+ test("scope profile missing project context does not write outside readable layers", () => {
698
+ const config = parseConfig({
699
+ namespacesEnabled: true,
700
+ defaultNamespace: "default",
701
+ sharedNamespace: "shared",
702
+ namespacePolicies: [
703
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
704
+ { name: "shared", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
705
+ ],
706
+ scopeProfiles: {
707
+ teamCoding: {
708
+ readOrder: ["userProject", "serverShared"],
709
+ writeDefault: "userProject",
710
+ },
711
+ },
712
+ defaultScopeProfile: "teamCoding",
713
+ });
714
+
715
+ const plan = resolveScopeProfilePlan({
716
+ config,
717
+ principal: "pi-geek",
718
+ codingContext: null,
719
+ codingOverlay: null,
720
+ });
721
+
722
+ assert.ok(plan);
723
+ assert.equal(plan.writeLayer, "serverShared");
724
+ assert.equal(plan.writeNamespace, "shared");
725
+ assert.deepEqual(plan.readNamespaces, ["shared"]);
726
+ assert.ok(plan.warnings.some((warning) => warning.includes("writeDefault userProject unavailable")));
727
+ });
728
+
729
+ test("scope profile rejects unknown team-project namespace template placeholders", () => {
730
+ const config = parseConfig({
731
+ namespacesEnabled: true,
732
+ defaultNamespace: "default",
733
+ sharedNamespace: "shared",
734
+ namespacePolicies: [
735
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
736
+ ],
737
+ scopeProfiles: {
738
+ teamCoding: {
739
+ readOrder: ["teamProject"],
740
+ writeDefault: "teamProject",
741
+ promotionTargets: ["teamProject"],
742
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projecthash}" },
743
+ },
744
+ },
745
+ defaultScopeProfile: "teamCoding",
746
+ teams: {
747
+ pi: {
748
+ principals: ["pi-geek"],
749
+ read: ["pi-geek"],
750
+ write: ["pi-geek"],
751
+ promote: ["pi-geek"],
752
+ },
753
+ },
754
+ });
755
+ const overlay = resolveCodingNamespaceOverlay(codingContext, config.codingMode, config.defaultNamespace);
756
+ const plan = resolveScopeProfilePlan({ config, principal: "pi-geek", codingContext, codingOverlay: overlay });
757
+ assert.ok(plan);
758
+ const teamProject = plan.layers.find((layer) => layer.id === "teamProject");
759
+
760
+ assert.equal(teamProject?.readable, false);
761
+ assert.equal(teamProject?.writable, false);
762
+ assert.deepEqual(plan.readNamespaces, []);
763
+ assert.equal(teamProject?.reason, "unknown team-project namespace template placeholder(s): projecthash");
764
+ });
765
+
766
+ test("scope profile requires namespace policy access when team-project templates collide with protected namespaces", () => {
767
+ const config = parseConfig({
768
+ namespacesEnabled: true,
769
+ defaultNamespace: "default",
770
+ sharedNamespace: "shared",
771
+ namespacePolicies: [
772
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
773
+ { name: "shared", readPrincipals: ["pi-maintainer"], writePrincipals: ["pi-maintainer"] },
774
+ ],
775
+ scopeProfiles: {
776
+ teamCoding: {
777
+ readOrder: ["teamProject"],
778
+ writeDefault: "teamProject",
779
+ promotionTargets: ["teamProject"],
780
+ teamProject: { namespaceTemplate: "shared" },
781
+ },
782
+ },
783
+ defaultScopeProfile: "teamCoding",
784
+ teams: {
785
+ pi: {
786
+ principals: ["pi-geek"],
787
+ read: ["pi-geek"],
788
+ write: ["pi-geek"],
789
+ promote: ["pi-geek"],
790
+ },
791
+ },
792
+ });
793
+ const overlay = resolveCodingNamespaceOverlay(codingContext, config.codingMode, config.defaultNamespace);
794
+ const plan = resolveScopeProfilePlan({ config, principal: "pi-geek", codingContext, codingOverlay: overlay });
795
+ assert.ok(plan);
796
+ const teamProject = plan.layers.find((layer) => layer.id === "teamProject");
797
+
798
+ assert.equal(teamProject?.namespace, "shared");
799
+ assert.equal(teamProject?.readable, false);
800
+ assert.equal(teamProject?.writable, false);
801
+ assert.deepEqual(plan.readNamespaces, []);
802
+ assert.match(teamProject?.reason ?? "", /team-project namespace collides with a protected namespace policy/);
803
+ });
804
+
805
+
806
+
807
+
808
+
809
+ test("scope profile derives isolated safe namespace for unsafe principal ids", () => {
810
+ const config = parseConfig({
811
+ namespacesEnabled: true,
812
+ defaultNamespace: "default",
813
+ sharedNamespace: "shared",
814
+ namespacePolicies: [],
815
+ scopeProfiles: {
816
+ hosted: {
817
+ readOrder: ["userGlobal"],
818
+ writeDefault: "userGlobal",
819
+ },
820
+ },
821
+ defaultScopeProfile: "hosted",
822
+ });
823
+
824
+ const plan = resolveScopeProfilePlan({
825
+ config,
826
+ principal: "alice@example.com",
827
+ codingContext: null,
828
+ codingOverlay: null,
829
+ });
830
+
831
+ assert.ok(plan);
832
+ assert.match(plan.baseNamespace, /^principal-[a-f0-9]{54}$/);
833
+ assert.equal(plan.baseNamespace.length, 64);
834
+ assert.notEqual(plan.baseNamespace, "principal-" + stableHash("alice@example.com"));
835
+ const expected = plan.baseNamespace;
836
+ assert.deepEqual(plan.readNamespaces, [expected]);
837
+ assert.equal(plan.writeNamespace, expected);
838
+ });
839
+
840
+ test("scope profile explicit self namespace policy overrides implicit self access", () => {
841
+ const config = parseConfig({
842
+ namespacesEnabled: true,
843
+ defaultNamespace: "default",
844
+ sharedNamespace: "shared",
845
+ namespacePolicies: [
846
+ { name: "pi-geek", readPrincipals: [], writePrincipals: [] },
847
+ ],
848
+ scopeProfiles: {
849
+ teamCoding: {
850
+ readOrder: ["userGlobal"],
851
+ writeDefault: "userGlobal",
852
+ },
853
+ },
854
+ defaultScopeProfile: "teamCoding",
855
+ });
856
+
857
+ const plan = resolveScopeProfilePlan({
858
+ config,
859
+ principal: "pi-geek",
860
+ codingContext: null,
861
+ codingOverlay: null,
862
+ });
863
+
864
+ assert.ok(plan);
865
+ assert.equal(plan.baseNamespace, "pi-geek");
866
+ assert.deepEqual(plan.readNamespaces, []);
867
+ assert.equal(plan.writeNamespace, "");
868
+ const userGlobal = plan.layers.find((layer) => layer.id === "userGlobal");
869
+ assert.equal(userGlobal?.readable, false);
870
+ assert.equal(userGlobal?.writable, false);
871
+ });
872
+
873
+ test("scope profile auto-promotion is disabled by default", () => {
874
+ const config = parseConfig({
875
+ scopeProfiles: {
876
+ teamCoding: {
877
+ readOrder: ["userProject", "teamProject", "userGlobal", "serverShared"],
878
+ writeDefault: "userProject",
879
+ promotionTargets: ["teamProject", "serverShared"],
880
+ },
881
+ },
882
+ defaultScopeProfile: "teamCoding",
883
+ });
884
+
885
+ assert.equal(config.scopeProfiles.teamCoding.autoPromote.enabled, false);
886
+ assert.deepEqual(config.scopeProfiles.teamCoding.autoPromote.targets, []);
887
+ assert.deepEqual(config.scopeProfiles.teamCoding.autoPromote.categories, [
888
+ "fact",
889
+ "correction",
890
+ "decision",
891
+ "preference",
892
+ ]);
893
+ });
894
+
895
+ test("parseConfig rejects unsupported scope profile layers and targets", () => {
896
+ assert.throws(
897
+ () =>
898
+ parseConfig({
899
+ scopeProfiles: {
900
+ bad: {
901
+ readOrder: ["userProject", "otherUserProject"],
902
+ },
903
+ },
904
+ }),
905
+ /unsupported layer/,
906
+ );
907
+ assert.throws(
908
+ () =>
909
+ parseConfig({
910
+ scopeProfiles: {
911
+ bad: {
912
+ promotionTargets: ["../../shared"],
913
+ },
914
+ },
915
+ }),
916
+ /unsupported target/,
917
+ );
918
+ assert.throws(
919
+ () =>
920
+ parseConfig({
921
+ defaultScopeProfile: 42,
922
+ scopeProfiles: {
923
+ hosted: { readOrder: ["userProject"], writeDefault: "userProject" },
924
+ },
925
+ }),
926
+ /defaultScopeProfile must be a non-empty string/,
927
+ );
928
+ assert.throws(
929
+ () =>
930
+ parseConfig({
931
+ scopeProfiles: {
932
+ bad: {
933
+ autoPromote: true,
934
+ },
935
+ },
936
+ }),
937
+ /scopeProfiles.bad.autoPromote must be an object/,
938
+ );
939
+ assert.throws(
940
+ () =>
941
+ parseConfig({
942
+ scopeProfiles: {
943
+ bad: {
944
+ autoPromote: { enabled: "treu" },
945
+ },
946
+ },
947
+ }),
948
+ /autoPromote.enabled must be a boolean or boolean-like string/,
949
+ );
950
+ assert.throws(
951
+ () =>
952
+ parseConfig({
953
+ scopeProfiles: {
954
+ bad: {
955
+ autoPromote: { minConfidenceTier: "implide" },
956
+ },
957
+ },
958
+ }),
959
+ /minConfidenceTier must be one of/,
960
+ );
961
+ assert.throws(
962
+ () =>
963
+ parseConfig({
964
+ scopeProfiles: {
965
+ bad: {
966
+ autoPromote: { categories: ["decison"] },
967
+ },
968
+ },
969
+ }),
970
+ /autoPromote.categories must contain only/,
971
+ );
972
+ assert.throws(
973
+ () =>
974
+ parseConfig({
975
+ scopeProfiles: {
976
+ bad: {
977
+ autoPromote: { categories: "rule" },
978
+ },
979
+ },
980
+ }),
981
+ /autoPromote.categories must be an array/,
982
+ );
983
+ assert.throws(
984
+ () =>
985
+ parseConfig({
986
+ teams: {
987
+ core: {
988
+ read: "alice",
989
+ },
990
+ },
991
+ }),
992
+ /teams.core.read must be an array/,
993
+ );
994
+ assert.throws(
995
+ () =>
996
+ parseConfig({
997
+ teams: {
998
+ core: {
999
+ read: ["alice", 42],
1000
+ },
1001
+ },
1002
+ }),
1003
+ /teams.core.read must contain only non-empty strings/,
1004
+ );
1005
+ assert.throws(
1006
+ () =>
1007
+ parseConfig({
1008
+ teams: {
1009
+ core: {
1010
+ projectNamespaceTemplate: 42,
1011
+ },
1012
+ },
1013
+ }),
1014
+ /teams.core.projectNamespaceTemplate must be a non-empty string/,
1015
+ );
1016
+ assert.throws(
1017
+ () =>
1018
+ parseConfig({
1019
+ scopeProfiles: {
1020
+ bad: {
1021
+ teamProject: "pi",
1022
+ },
1023
+ },
1024
+ }),
1025
+ /scopeProfiles.bad.teamProject must be an object/,
1026
+ );
1027
+ assert.throws(
1028
+ () =>
1029
+ parseConfig({
1030
+ scopeProfiles: {
1031
+ bad: {
1032
+ readOrder: ["teamProject", "userGlobal"],
1033
+ writeDefault: "teamProject",
1034
+ teamProject: { teamId: 42 },
1035
+ },
1036
+ },
1037
+ }),
1038
+ /scopeProfiles.bad.teamProject.teamId must be a non-empty string/,
1039
+ );
1040
+ assert.throws(
1041
+ () =>
1042
+ parseConfig({
1043
+ scopeProfiles: {
1044
+ bad: {
1045
+ readOrder: ["teamProject", "userGlobal"],
1046
+ writeDefault: "teamProject",
1047
+ teamProject: { namespaceTemplate: "" },
1048
+ },
1049
+ },
1050
+ }),
1051
+ /scopeProfiles.bad.teamProject.namespaceTemplate must be a non-empty string/,
1052
+ );
1053
+ assert.throws(
1054
+ () =>
1055
+ parseConfig({
1056
+ scopeProfiles: {
1057
+ bad: {
1058
+ readOrder: ["teamProject", "userGlobal"],
1059
+ writeDefault: "teamProject",
1060
+ teamProject: { teamId: "missing" },
1061
+ },
1062
+ },
1063
+ teams: {
1064
+ core: {
1065
+ principals: ["alice"],
1066
+ read: ["alice"],
1067
+ write: ["alice"],
1068
+ promote: ["alice"],
1069
+ },
1070
+ },
1071
+ }),
1072
+ /scopeProfiles.bad.teamProject.teamId references unknown team: missing/,
1073
+ );
1074
+ });