@remnic/core 9.3.675 → 9.3.677
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-cli.js +16 -16
- package/dist/access-http.js +13 -13
- package/dist/access-mcp.js +12 -12
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +10 -10
- package/dist/{chunk-OG7A6AZX.js → chunk-2DKXY243.js} +4 -4
- package/dist/{chunk-Q5ZU3RNY.js → chunk-57ME5VSI.js} +4 -4
- package/dist/{chunk-SDLJ2W7S.js → chunk-7UTCHQTB.js} +2 -2
- package/dist/{chunk-T2AOOHDA.js → chunk-ACYX37IM.js} +2 -2
- package/dist/{chunk-ZLINDOBG.js → chunk-CZMLLVU2.js} +3 -3
- package/dist/{chunk-DOCTITOP.js → chunk-DGEZKYVI.js} +4 -4
- package/dist/{chunk-Q6MIDQEL.js → chunk-EQYP3HA6.js} +2 -2
- package/dist/{chunk-52LZ42LI.js → chunk-ERA5RSMZ.js} +1 -1
- package/dist/{chunk-IPLYGWQF.js → chunk-KQAFEZQX.js} +5 -5
- package/dist/{chunk-SF45RQDX.js → chunk-RP64QP7G.js} +3 -3
- package/dist/{chunk-QLRYXOAD.js → chunk-UDJLF3BO.js} +2 -2
- package/dist/{chunk-R37A3BEW.js → chunk-YEQBJXVO.js} +111 -101
- package/dist/chunk-YEQBJXVO.js.map +1 -0
- package/dist/{chunk-B55KFEGS.js → chunk-YJ4J2JJ2.js} +10 -10
- package/dist/{chunk-XVVEKF5I.js → chunk-Z56KDLDK.js} +20 -20
- package/dist/{chunk-OUWAQVDJ.js → chunk-Z6SEG36L.js} +4 -4
- package/dist/cli.js +22 -22
- package/dist/{coding-graph-types-Dd2tGrnm.d.ts → coding/coding-graph-types.d.ts} +1 -1
- package/dist/coding/coding-graph-types.js +10 -0
- package/dist/coding/coding-graph-types.js.map +1 -0
- package/dist/coding/optional-coding-graph.d.ts +2 -2
- package/dist/coding/optional-coding-graph.js +1 -1
- package/dist/contradiction/index.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +33 -33
- package/dist/lcm/index.js +3 -3
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/search.js +7 -7
- package/dist/operator-toolkit.js +9 -9
- package/dist/orchestrator.js +13 -13
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +11 -11
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +7 -2
- package/src/orchestrator.ts +50 -197
- package/src/scopes/scope-plan.test.ts +360 -0
- package/src/scopes/scope-plan.ts +320 -0
- package/dist/chunk-R37A3BEW.js.map +0 -1
- /package/dist/{chunk-OG7A6AZX.js.map → chunk-2DKXY243.js.map} +0 -0
- /package/dist/{chunk-Q5ZU3RNY.js.map → chunk-57ME5VSI.js.map} +0 -0
- /package/dist/{chunk-SDLJ2W7S.js.map → chunk-7UTCHQTB.js.map} +0 -0
- /package/dist/{chunk-T2AOOHDA.js.map → chunk-ACYX37IM.js.map} +0 -0
- /package/dist/{chunk-ZLINDOBG.js.map → chunk-CZMLLVU2.js.map} +0 -0
- /package/dist/{chunk-DOCTITOP.js.map → chunk-DGEZKYVI.js.map} +0 -0
- /package/dist/{chunk-Q6MIDQEL.js.map → chunk-EQYP3HA6.js.map} +0 -0
- /package/dist/{chunk-52LZ42LI.js.map → chunk-ERA5RSMZ.js.map} +0 -0
- /package/dist/{chunk-IPLYGWQF.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-SF45RQDX.js.map → chunk-RP64QP7G.js.map} +0 -0
- /package/dist/{chunk-QLRYXOAD.js.map → chunk-UDJLF3BO.js.map} +0 -0
- /package/dist/{chunk-B55KFEGS.js.map → chunk-YJ4J2JJ2.js.map} +0 -0
- /package/dist/{chunk-XVVEKF5I.js.map → chunk-Z56KDLDK.js.map} +0 -0
- /package/dist/{chunk-OUWAQVDJ.js.map → chunk-Z6SEG36L.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -325,6 +325,7 @@ import {
|
|
|
325
325
|
recallNamespacesForPrincipal,
|
|
326
326
|
resolvePrincipal,
|
|
327
327
|
} from "./namespaces/principal.js";
|
|
328
|
+
import { resolveScopePlan } from "./scopes/scope-plan.js";
|
|
328
329
|
import {
|
|
329
330
|
expandScopeProfileReadNamespaces,
|
|
330
331
|
resolveScopeProfilePlan,
|
|
@@ -5770,7 +5771,8 @@ export class Orchestrator {
|
|
|
5770
5771
|
options.principalOverride.length > 0
|
|
5771
5772
|
? options.principalOverride
|
|
5772
5773
|
: resolvePrincipal(sessionKey, this.config);
|
|
5773
|
-
|
|
5774
|
+
const namespacesEnabled = this.config.namespacesEnabled;
|
|
5775
|
+
if (namespacesEnabled && !principal) {
|
|
5774
5776
|
throw new Error("authentication required: namespaces are enabled and no principal was supplied");
|
|
5775
5777
|
}
|
|
5776
5778
|
|
|
@@ -5862,6 +5864,7 @@ export class Orchestrator {
|
|
|
5862
5864
|
options.namespace?.trim() || undefined,
|
|
5863
5865
|
options.principalOverride,
|
|
5864
5866
|
caps,
|
|
5867
|
+
namespacesEnabled,
|
|
5865
5868
|
);
|
|
5866
5869
|
} catch (err) {
|
|
5867
5870
|
log.debug(`direct-answer observation setup failed: ${err}`);
|
|
@@ -5931,62 +5934,27 @@ export class Orchestrator {
|
|
|
5931
5934
|
namespaceOverride: string | undefined,
|
|
5932
5935
|
principalOverride: string | undefined,
|
|
5933
5936
|
caps: CapabilitySet,
|
|
5937
|
+
namespacesEnabled: boolean,
|
|
5934
5938
|
): void {
|
|
5935
5939
|
const expectedSnapshot = this.lastRecall.get(sessionKey);
|
|
5936
5940
|
if (expectedSnapshot === null) return;
|
|
5937
5941
|
if (expectedSnapshot.plannerMode === "no_recall") return;
|
|
5938
5942
|
|
|
5939
|
-
|
|
5940
|
-
//
|
|
5941
|
-
//
|
|
5942
|
-
// the
|
|
5943
|
-
const
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
: resolveScopeProfilePlan({
|
|
5955
|
-
config: this.config,
|
|
5956
|
-
principal,
|
|
5957
|
-
codingContext: sessionKey
|
|
5958
|
-
? this.getCodingContextForSession(sessionKey)
|
|
5959
|
-
: null,
|
|
5960
|
-
codingOverlay: observationCodingOverlay,
|
|
5961
|
-
});
|
|
5962
|
-
let observationNamespaces: string[];
|
|
5963
|
-
if (namespaceOverride && canReadNamespace(principal, namespaceOverride, this.config)) {
|
|
5964
|
-
observationNamespaces = [namespaceOverride];
|
|
5965
|
-
} else if (observationScopeProfilePlan) {
|
|
5966
|
-
observationNamespaces = expandScopeProfileReadNamespaces({
|
|
5967
|
-
profilePlan: observationScopeProfilePlan,
|
|
5968
|
-
principalSelfNamespace: observationScopeProfilePlan.baseNamespace,
|
|
5969
|
-
config: this.config,
|
|
5970
|
-
principal,
|
|
5971
|
-
codingOverlay: observationCodingOverlay,
|
|
5972
|
-
legacyRecallNamespaces: recallNamespacesForPrincipal(principal, this.config),
|
|
5973
|
-
});
|
|
5974
|
-
} else if (observationCodingOverlay && observationCodingSelf) {
|
|
5975
|
-
// Rule 42 / parity with the main recall path: substitute the self
|
|
5976
|
-
// namespace within the principal's recall list rather than
|
|
5977
|
-
// replacing the full list. Preserves shared and policy-include
|
|
5978
|
-
// namespaces for direct-answer observation queries.
|
|
5979
|
-
const base = recallNamespacesForPrincipal(principal, this.config);
|
|
5980
|
-
const mapped = base.map((ns) =>
|
|
5981
|
-
ns === observationPrincipalSelf ? observationCodingSelf : ns,
|
|
5982
|
-
);
|
|
5983
|
-
const fallbackNs = observationCodingOverlay.readFallbacks.map((fallback) =>
|
|
5984
|
-
combineNamespaces(observationPrincipalSelf, fallback),
|
|
5985
|
-
);
|
|
5986
|
-
observationNamespaces = Array.from(new Set<string>([...mapped, ...fallbackNs]));
|
|
5987
|
-
} else {
|
|
5988
|
-
observationNamespaces = recallNamespacesForPrincipal(principal, this.config);
|
|
5989
|
-
}
|
|
5943
|
+
// Resolve the observation namespace set through the SAME ScopePlan resolver
|
|
5944
|
+
// the main recall path uses (#1521). The observe path does NOT throw on an
|
|
5945
|
+
// unreadable override (it falls through to the coding/legacy branches), so
|
|
5946
|
+
// we skip the readability gate the recall path enforces.
|
|
5947
|
+
const observationScopePlan = resolveScopePlan({
|
|
5948
|
+
config: this.config,
|
|
5949
|
+
sessionKey,
|
|
5950
|
+
namespace: namespaceOverride,
|
|
5951
|
+
principalOverride,
|
|
5952
|
+
codingContext: sessionKey
|
|
5953
|
+
? this.getCodingContextForSession(sessionKey)
|
|
5954
|
+
: null,
|
|
5955
|
+
namespacesEnabled,
|
|
5956
|
+
});
|
|
5957
|
+
const observationNamespaces = observationScopePlan.readNamespaces;
|
|
5990
5958
|
const observationQueryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
|
|
5991
5959
|
cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
|
|
5992
5960
|
cronRecallNormalizedQueryMaxChars:
|
|
@@ -7587,14 +7555,11 @@ export class Orchestrator {
|
|
|
7587
7555
|
&& options.principalOverride.length > 0
|
|
7588
7556
|
? options.principalOverride
|
|
7589
7557
|
: resolvePrincipal(sessionKey, this.config);
|
|
7590
|
-
|
|
7558
|
+
const namespacesEnabled = this.config.namespacesEnabled;
|
|
7559
|
+
if (namespacesEnabled && !principal) {
|
|
7591
7560
|
throw new Error("authentication required: namespaces are enabled and no principal was supplied");
|
|
7592
7561
|
}
|
|
7593
7562
|
const namespaceOverride = options.namespace?.trim() || undefined;
|
|
7594
|
-
const readableRecallNamespaces = recallNamespacesForPrincipal(
|
|
7595
|
-
principal,
|
|
7596
|
-
this.config,
|
|
7597
|
-
);
|
|
7598
7563
|
if (
|
|
7599
7564
|
namespaceOverride &&
|
|
7600
7565
|
!canReadNamespace(principal, namespaceOverride, this.config)
|
|
@@ -7603,147 +7568,35 @@ export class Orchestrator {
|
|
|
7603
7568
|
`namespace override is not readable: ${namespaceOverride}`,
|
|
7604
7569
|
);
|
|
7605
7570
|
}
|
|
7606
|
-
//
|
|
7607
|
-
// the
|
|
7608
|
-
//
|
|
7609
|
-
//
|
|
7610
|
-
//
|
|
7611
|
-
// principal's recall list — it does NOT replace the full list. Shared
|
|
7612
|
-
// and `includeInRecallByDefault` policy namespaces stay in the recall
|
|
7613
|
-
// set so coding sessions continue to see team/shared memories. The
|
|
7614
|
-
// overlay is combined with the principal base through `combineNamespaces`
|
|
7615
|
-
// to preserve principal isolation (cross-tenant leakage guard).
|
|
7616
|
-
const codingOverlay = namespaceOverride ? null : this.applyCodingRecallOverlay(sessionKey);
|
|
7617
|
-
const principalSelfNamespace = defaultNamespaceForPrincipal(principal, this.config);
|
|
7618
|
-
const codingSelfNamespace = codingOverlay
|
|
7619
|
-
? combineNamespaces(principalSelfNamespace, codingOverlay.namespace)
|
|
7620
|
-
: null;
|
|
7621
|
-
const scopeProfilePlan = namespaceOverride
|
|
7622
|
-
? null
|
|
7623
|
-
: resolveScopeProfilePlan({
|
|
7624
|
-
config: this.config,
|
|
7625
|
-
principal,
|
|
7626
|
-
codingContext: sessionKey
|
|
7627
|
-
? this.getCodingContextForSession(sessionKey)
|
|
7628
|
-
: null,
|
|
7629
|
-
codingOverlay,
|
|
7630
|
-
});
|
|
7631
|
-
const profileEffectiveNamespace = scopeProfilePlan?.writeNamespace || scopeProfilePlan?.readNamespaces[0];
|
|
7632
|
-
const selfNamespace =
|
|
7633
|
-
namespaceOverride ??
|
|
7634
|
-
profileEffectiveNamespace ??
|
|
7635
|
-
codingSelfNamespace ??
|
|
7636
|
-
principalSelfNamespace;
|
|
7637
|
-
let recallNamespaces: string[];
|
|
7638
|
-
if (namespaceOverride) {
|
|
7639
|
-
recallNamespaces = [namespaceOverride];
|
|
7640
|
-
} else if (scopeProfilePlan) {
|
|
7641
|
-
recallNamespaces = expandScopeProfileReadNamespaces({
|
|
7642
|
-
profilePlan: scopeProfilePlan,
|
|
7643
|
-
principalSelfNamespace: scopeProfilePlan.baseNamespace,
|
|
7644
|
-
config: this.config,
|
|
7645
|
-
principal,
|
|
7646
|
-
codingOverlay,
|
|
7647
|
-
legacyRecallNamespaces: readableRecallNamespaces,
|
|
7648
|
-
});
|
|
7649
|
-
} else if (codingOverlay && codingSelfNamespace) {
|
|
7650
|
-
// Substitute the principal's self namespace with the coding-scoped
|
|
7651
|
-
// one, and append any read fallbacks (branch→project, PR 3) combined
|
|
7652
|
-
// with the principal base so principal isolation is preserved on
|
|
7653
|
-
// fallback entries as well.
|
|
7654
|
-
const mapped = readableRecallNamespaces.map((ns) =>
|
|
7655
|
-
ns === principalSelfNamespace ? codingSelfNamespace : ns,
|
|
7656
|
-
);
|
|
7657
|
-
const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>
|
|
7658
|
-
combineNamespaces(principalSelfNamespace, fallback),
|
|
7659
|
-
);
|
|
7660
|
-
recallNamespaces = Array.from(new Set<string>([...mapped, ...fallbackNs]));
|
|
7661
|
-
} else {
|
|
7662
|
-
recallNamespaces = readableRecallNamespaces;
|
|
7663
|
-
}
|
|
7664
|
-
// Catalog touch (issue #1499): record reads against the recalled namespaces
|
|
7665
|
-
// so the catalog reflects active read scopes. Best-effort, failure-tolerant.
|
|
7666
|
-
// Round 3 (codex P2): gate behind the no_recall guard — when the planner
|
|
7667
|
-
// selects `no_recall` retrieval is skipped entirely (see the early return at
|
|
7668
|
-
// `recallMode === "no_recall"` below), so marking every readable namespace as
|
|
7669
|
-
// read would falsely inflate `lastReadAt` / catalog recency.
|
|
7670
|
-
// Round 4 (codex P2): also skip when the effective memory result limit is
|
|
7671
|
-
// zero (`topK: 0`, a disabled/zero `memories` recall section, etc.). The QMD
|
|
7672
|
-
// path explicitly returns before searching when `recallResultLimit <= 0`, so
|
|
7673
|
-
// no namespace is actually read and the touch would be spurious.
|
|
7674
|
-
// NOTE: the catalog read touch is recorded LATER, immediately after the
|
|
7675
|
-
// Phase 1 `throwIfRecallAborted` gate (round 6, codex P2 / cursor Medium —
|
|
7676
|
-
// NDXHa/NDmle), so it fires only once retrieval is actually about to run.
|
|
7677
|
-
// Recording it here (recall entry) would set `lastReadAt` for recalls that
|
|
7678
|
-
// are aborted, error out, or short-circuit before any QMD/filesystem read.
|
|
7679
|
-
|
|
7680
|
-
// Effective LCM read NAMESPACE SET (#1505 thread "Include coding fallback
|
|
7681
|
-
// namespaces in LCM reads"). `observe` archives LCM / structured history
|
|
7682
|
-
// under `${effectiveNamespace}:${sessionKey}` for whichever namespace was
|
|
7683
|
-
// effective at write time. A branch-scoped session whose evidence was
|
|
7684
|
-
// archived at project / root scope must still surface it, exactly as normal
|
|
7685
|
-
// QMD/file recall does — QMD/file recall searches the primary overlay key AND
|
|
7686
|
-
// `codingOverlay.readFallbacks` (project / root), NOT just the primary
|
|
7687
|
-
// overlay key. The prior single `lcmReadSessionId` only targeted the primary
|
|
7688
|
-
// overlay, so branch-scoped sessions missed fallback LCM evidence.
|
|
7571
|
+
// Resolve every namespace-bearing field through ONE ScopePlan (#1521): the
|
|
7572
|
+
// read set, the LCM read keys, the coding overlay, and the scope-profile plan
|
|
7573
|
+
// all come from a single pure resolver that delegates to the same helpers
|
|
7574
|
+
// the inline code used. Parity snapshots in scope-plan.test.ts pin the
|
|
7575
|
+
// outputs so this migration cannot change behavior.
|
|
7689
7576
|
//
|
|
7690
|
-
//
|
|
7691
|
-
//
|
|
7692
|
-
//
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
// Explicit namespace already read-authorized above (canReadNamespace gate).
|
|
7714
|
-
lcmReadNamespaces = [namespaceOverride];
|
|
7715
|
-
} else if (scopeProfilePlan) {
|
|
7716
|
-
// Scope profiles define a layered read stack; LCM-backed evidence uses the
|
|
7717
|
-
// same namespace set as QMD/file recall so team/global/shared observations
|
|
7718
|
-
// are not silently skipped.
|
|
7719
|
-
lcmReadNamespaces = recallNamespaces;
|
|
7720
|
-
} else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {
|
|
7721
|
-
// Self base readable → overlay rows authorized. Read the primary overlay
|
|
7722
|
-
// key first, then each coding read fallback (project → root), combined with
|
|
7723
|
-
// the principal base for isolation — the SAME ordered set QMD/file recall
|
|
7724
|
-
// searches for this authorized coding session.
|
|
7725
|
-
const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>
|
|
7726
|
-
combineNamespaces(principalSelfNamespace, fallback),
|
|
7727
|
-
);
|
|
7728
|
-
lcmReadNamespaces = [codingSelfNamespace, ...fallbackNs];
|
|
7729
|
-
} else {
|
|
7730
|
-
// No overlay, OR overlay present but self base unreadable → collapse to the
|
|
7731
|
-
// default store (raw sessionKey), exactly as the prior round did. No
|
|
7732
|
-
// `<principal>-project-*` overlay key is searched.
|
|
7733
|
-
lcmReadNamespaces = [this.config.defaultNamespace];
|
|
7734
|
-
}
|
|
7735
|
-
// Map the ordered, read-authorized namespace set → ordered, deduped LCM read
|
|
7736
|
-
// session_id set. Single-user / no-overlay recall passes a single-namespace
|
|
7737
|
-
// set that collapses to the raw `sessionKey`, so this is `[sessionKey]` —
|
|
7738
|
-
// byte-for-byte the pre-#1495 single-key behavior.
|
|
7739
|
-
const lcmReadSessionIds =
|
|
7740
|
-
scopeProfilePlan && !sessionKey
|
|
7741
|
-
? []
|
|
7742
|
-
: lcmReadSessionIdsForNamespaces(
|
|
7743
|
-
lcmReadNamespaces,
|
|
7744
|
-
sessionKey,
|
|
7745
|
-
this.config.defaultNamespace,
|
|
7746
|
-
);
|
|
7577
|
+
// Catalog read touch (issue #1499) is recorded LATER — after the Phase 1
|
|
7578
|
+
// abort gate — so it fires only when retrieval actually runs, not for
|
|
7579
|
+
// aborted / short-circuited recalls.
|
|
7580
|
+
const scopePlan = resolveScopePlan({
|
|
7581
|
+
config: this.config,
|
|
7582
|
+
sessionKey,
|
|
7583
|
+
namespace: options.namespace,
|
|
7584
|
+
principalOverride:
|
|
7585
|
+
typeof options.principalOverride === "string"
|
|
7586
|
+
&& options.principalOverride.length > 0
|
|
7587
|
+
? options.principalOverride
|
|
7588
|
+
: undefined,
|
|
7589
|
+
codingContext: sessionKey
|
|
7590
|
+
? this.getCodingContextForSession(sessionKey)
|
|
7591
|
+
: null,
|
|
7592
|
+
namespacesEnabled,
|
|
7593
|
+
});
|
|
7594
|
+
const {
|
|
7595
|
+
readNamespaces: recallNamespaces,
|
|
7596
|
+
baseNamespace: selfNamespace,
|
|
7597
|
+
scopeProfilePlan,
|
|
7598
|
+
lcmReadSessionIds,
|
|
7599
|
+
} = scopePlan;
|
|
7747
7600
|
// Query an LCM-backed read across the ordered read key set and return the
|
|
7748
7601
|
// FIRST non-empty result (#1505 fallback-namespace unification). The primary
|
|
7749
7602
|
// overlay key is tried first; if a branch-scoped session has no rows under its
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScopePlan resolver parity tests (issue #1521 step 2).
|
|
3
|
+
*
|
|
4
|
+
* These tests snapshot the effective namespace sets the resolver produces for a
|
|
5
|
+
* fixed matrix of inputs. The snapshots were derived by tracing the pre-migration
|
|
6
|
+
* inline resolution in `orchestrator.recallInternal` and
|
|
7
|
+
* `orchestrator.enqueueDirectAnswerObservation` — the SAME helpers in the SAME
|
|
8
|
+
* order. They MUST NOT change when consumers switch from the inline code to the
|
|
9
|
+
* resolver; if they do, the resolver diverged and must be corrected before
|
|
10
|
+
* migration lands.
|
|
11
|
+
*
|
|
12
|
+
* Input matrix (issue #1521 step 2):
|
|
13
|
+
* - default namespace (no policies, no coding context)
|
|
14
|
+
* - named namespace (explicit override, readable)
|
|
15
|
+
* - coding overlay (project scope and branch scope)
|
|
16
|
+
* - sparse metadata (empty/missing fields, no session key)
|
|
17
|
+
* - legacy `agent:*` session keys
|
|
18
|
+
* - scope-profile plan (active profile)
|
|
19
|
+
* - explicit namespace override (unreadable falls through)
|
|
20
|
+
*/
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import test from "node:test";
|
|
23
|
+
|
|
24
|
+
import { resolveScopePlan } from "./scope-plan.js";
|
|
25
|
+
import {
|
|
26
|
+
combineNamespaces,
|
|
27
|
+
lcmSessionKeyForNamespace,
|
|
28
|
+
projectNamespaceName,
|
|
29
|
+
} from "../coding/coding-namespace.js";
|
|
30
|
+
import type { CodingContext, PluginConfig } from "../types.js";
|
|
31
|
+
|
|
32
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// Config builders
|
|
34
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function baseConfig(overrides: Partial<PluginConfig> = {}): PluginConfig {
|
|
37
|
+
return {
|
|
38
|
+
namespacesEnabled: true,
|
|
39
|
+
defaultNamespace: "default",
|
|
40
|
+
sharedNamespace: "shared",
|
|
41
|
+
namespacePolicies: [],
|
|
42
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
43
|
+
codingMode: { projectScope: true, branchScope: false, globalFallback: true },
|
|
44
|
+
principalFromSessionKeyMode: "prefix",
|
|
45
|
+
principalFromSessionKeyRules: [],
|
|
46
|
+
scopeProfiles: {},
|
|
47
|
+
defaultScopeProfile: undefined,
|
|
48
|
+
teams: {},
|
|
49
|
+
...overrides,
|
|
50
|
+
} as unknown as PluginConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A principal whose self namespace exists as a policy, so the overlay base is
|
|
54
|
+
* non-default and readable. */
|
|
55
|
+
function withSelfPolicy(config: PluginConfig, principal: string): PluginConfig {
|
|
56
|
+
return {
|
|
57
|
+
...config,
|
|
58
|
+
namespacePolicies: [
|
|
59
|
+
{ name: principal, readPrincipals: [principal], writePrincipals: [principal] },
|
|
60
|
+
...(config.namespacePolicies ?? []),
|
|
61
|
+
],
|
|
62
|
+
principalFromSessionKeyMode: "prefix",
|
|
63
|
+
principalFromSessionKeyRules: [
|
|
64
|
+
{ match: `${principal}:`, principal },
|
|
65
|
+
...(config.principalFromSessionKeyRules ?? []),
|
|
66
|
+
],
|
|
67
|
+
} as unknown as PluginConfig;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function codingContext(projectId: string, branch: string | null = null): CodingContext {
|
|
71
|
+
return { projectId, branch, rootPath: "/repo", defaultBranch: "main" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// Snapshot 1: default namespace, no coding context
|
|
76
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
test("scope-plan: default namespace, no coding context → [default]", () => {
|
|
79
|
+
const config = baseConfig();
|
|
80
|
+
const plan = resolveScopePlan({
|
|
81
|
+
config,
|
|
82
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
83
|
+
sessionKey: "sess-1",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
assert.equal(plan.principal, "default");
|
|
87
|
+
assert.equal(plan.namespaceOverride, undefined);
|
|
88
|
+
assert.equal(plan.baseNamespace, "default");
|
|
89
|
+
assert.deepEqual(plan.readNamespaces, ["default", "shared"]);
|
|
90
|
+
assert.deepEqual(plan.readFallbacks, []);
|
|
91
|
+
assert.deepEqual(plan.lcmReadNamespaces, ["default"]);
|
|
92
|
+
assert.equal(plan.codingOverlay, null);
|
|
93
|
+
assert.equal(plan.scopeProfilePlan, null);
|
|
94
|
+
// LCM key is the raw sessionKey (default store, no overlay).
|
|
95
|
+
assert.deepEqual([...plan.lcmReadSessionIds], ["sess-1"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
99
|
+
// Snapshot 2: named namespace (explicit, readable override)
|
|
100
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
test("scope-plan: explicit readable namespace override wins", () => {
|
|
103
|
+
const config = baseConfig({
|
|
104
|
+
namespacePolicies: [
|
|
105
|
+
{ name: "team-data", readPrincipals: ["default"], writePrincipals: [] },
|
|
106
|
+
],
|
|
107
|
+
} as Partial<PluginConfig>);
|
|
108
|
+
const plan = resolveScopePlan({
|
|
109
|
+
config,
|
|
110
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
111
|
+
sessionKey: "sess-1",
|
|
112
|
+
namespace: "team-data",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
assert.equal(plan.namespaceOverride, "team-data");
|
|
116
|
+
assert.equal(plan.baseNamespace, "team-data");
|
|
117
|
+
assert.deepEqual(plan.readNamespaces, ["team-data"]);
|
|
118
|
+
assert.deepEqual(plan.lcmReadNamespaces, ["team-data"]);
|
|
119
|
+
assert.equal(plan.codingOverlay, null);
|
|
120
|
+
assert.equal(plan.scopeProfilePlan, null);
|
|
121
|
+
assert.deepEqual(
|
|
122
|
+
[...plan.lcmReadSessionIds],
|
|
123
|
+
[lcmSessionKeyForNamespace("team-data", "sess-1", "default")],
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// Snapshot 3: coding overlay (project scope)
|
|
129
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
test("scope-plan: coding overlay (project scope) substitutes self base", () => {
|
|
132
|
+
const config = withSelfPolicy(baseConfig(), "alice");
|
|
133
|
+
const ctx = codingContext("myproj");
|
|
134
|
+
const plan = resolveScopePlan({
|
|
135
|
+
config,
|
|
136
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
137
|
+
sessionKey: "alice:sess-1",
|
|
138
|
+
codingContext: ctx,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const projectNs = projectNamespaceName("myproj");
|
|
142
|
+
const codingSelf = combineNamespaces("alice", projectNs);
|
|
143
|
+
|
|
144
|
+
assert.equal(plan.principal, "alice");
|
|
145
|
+
assert.equal(plan.baseNamespace, codingSelf);
|
|
146
|
+
// readNamespaces substitutes "alice" → codingSelf, keeps shared.
|
|
147
|
+
// globalFallback=true adds the root ("") fallback: combineNamespaces("alice",
|
|
148
|
+
// "") → "alice", so the principal's own namespace appears as a read fallback.
|
|
149
|
+
assert.deepEqual(plan.readNamespaces, [codingSelf, "shared", "alice"]);
|
|
150
|
+
assert.deepEqual(plan.readFallbacks, ["alice"]);
|
|
151
|
+
assert.deepEqual(plan.lcmReadNamespaces, [codingSelf, "alice"]);
|
|
152
|
+
assert.equal(plan.codingOverlay?.namespace, projectNs);
|
|
153
|
+
assert.deepEqual([...plan.codingOverlay?.readFallbacks ?? []], [""]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
157
|
+
// Snapshot 4: coding overlay (branch scope) appends project fallback
|
|
158
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
test("scope-plan: coding overlay (branch scope) appends project + root fallbacks", () => {
|
|
161
|
+
const config = withSelfPolicy(
|
|
162
|
+
baseConfig({
|
|
163
|
+
codingMode: { projectScope: true, branchScope: true, globalFallback: true },
|
|
164
|
+
} as Partial<PluginConfig>),
|
|
165
|
+
"alice",
|
|
166
|
+
);
|
|
167
|
+
const ctx = codingContext("myproj", "feature-x");
|
|
168
|
+
const plan = resolveScopePlan({
|
|
169
|
+
config,
|
|
170
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
171
|
+
sessionKey: "alice:sess-1",
|
|
172
|
+
codingContext: ctx,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Verify the key invariants: overlay is non-null, base is combined with self.
|
|
176
|
+
assert.notEqual(plan.codingOverlay, null);
|
|
177
|
+
assert.notEqual(plan.baseNamespace, "alice");
|
|
178
|
+
// readNamespaces includes the coding self (branch) and fallbacks.
|
|
179
|
+
assert.ok(plan.readNamespaces.length >= 2, "branch scope must include fallbacks");
|
|
180
|
+
// LCM read includes coding self + fallbacks.
|
|
181
|
+
assert.ok(plan.lcmReadNamespaces.length >= 2, "LCM must include fallback keys");
|
|
182
|
+
// readFallbacks is non-empty (project + root when globalFallback).
|
|
183
|
+
assert.ok(plan.readFallbacks.length >= 1, "branch scope has at least project fallback");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
187
|
+
// Snapshot 5: sparse metadata — no session key
|
|
188
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
test("scope-plan: no session key → principal undefined, default namespace", () => {
|
|
191
|
+
const config = baseConfig();
|
|
192
|
+
const plan = resolveScopePlan({
|
|
193
|
+
config,
|
|
194
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
assert.equal(plan.principal, undefined);
|
|
198
|
+
assert.equal(plan.baseNamespace, "default");
|
|
199
|
+
// recallNamespacesForPrincipal(undefined) → [] (no principal).
|
|
200
|
+
assert.deepEqual(plan.readNamespaces, []);
|
|
201
|
+
assert.deepEqual(plan.lcmReadNamespaces, ["default"]);
|
|
202
|
+
// Sessionless LCM → [undefined] (archive-wide read, no session_id filter).
|
|
203
|
+
assert.deepEqual([...plan.lcmReadSessionIds], [undefined]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
207
|
+
// Snapshot 6: legacy agent:* session key
|
|
208
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
test("scope-plan: legacy agent:* session key resolves principal via heuristic", () => {
|
|
211
|
+
const config = baseConfig();
|
|
212
|
+
const plan = resolveScopePlan({
|
|
213
|
+
config,
|
|
214
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
215
|
+
sessionKey: "agent:bot-1:slack:chan-1",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// resolvePrincipal heuristic: parts[0] === "agent" → parts[1] = "bot-1".
|
|
219
|
+
assert.equal(plan.principal, "bot-1");
|
|
220
|
+
// No policy for "bot-1" → defaultNamespaceForPrincipal → "default".
|
|
221
|
+
assert.equal(plan.baseNamespace, "default");
|
|
222
|
+
// recallNamespacesForPrincipal("bot-1"): self="default" (readable), shared.
|
|
223
|
+
assert.deepEqual(plan.readNamespaces, ["default", "shared"]);
|
|
224
|
+
assert.deepEqual(plan.lcmReadNamespaces, ["default"]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
228
|
+
// Snapshot 7: namespacesEnabled false → single-store collapse
|
|
229
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
test("scope-plan: namespacesEnabled false collapses to default store", () => {
|
|
232
|
+
const config = baseConfig({ namespacesEnabled: false } as Partial<PluginConfig>);
|
|
233
|
+
const plan = resolveScopePlan({
|
|
234
|
+
config,
|
|
235
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
236
|
+
sessionKey: "sess-1",
|
|
237
|
+
codingContext: codingContext("myproj"),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// resolvePrincipal returns "default" when namespaces disabled.
|
|
241
|
+
assert.equal(plan.principal, "default");
|
|
242
|
+
assert.equal(plan.baseNamespace, "default");
|
|
243
|
+
assert.deepEqual(plan.readNamespaces, ["default"]);
|
|
244
|
+
assert.equal(plan.codingOverlay, null);
|
|
245
|
+
assert.equal(plan.scopeProfilePlan, null);
|
|
246
|
+
// Single store → raw sessionKey.
|
|
247
|
+
assert.deepEqual([...plan.lcmReadSessionIds], ["sess-1"]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
251
|
+
// Snapshot 8: codingMode.projectScope false → no overlay
|
|
252
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
test("scope-plan: projectScope false → no overlay even with coding context", () => {
|
|
255
|
+
const config = withSelfPolicy(
|
|
256
|
+
baseConfig({
|
|
257
|
+
codingMode: { projectScope: false, branchScope: false, globalFallback: true },
|
|
258
|
+
} as Partial<PluginConfig>),
|
|
259
|
+
"alice",
|
|
260
|
+
);
|
|
261
|
+
const plan = resolveScopePlan({
|
|
262
|
+
config,
|
|
263
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
264
|
+
sessionKey: "alice:sess-1",
|
|
265
|
+
codingContext: codingContext("myproj"),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
assert.equal(plan.codingOverlay, null);
|
|
269
|
+
assert.equal(plan.baseNamespace, "alice");
|
|
270
|
+
// No overlay → readable recall set unchanged (self substituted by nothing).
|
|
271
|
+
assert.deepEqual(plan.readNamespaces, ["alice", "shared"]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
275
|
+
// Snapshot 9: explicit override not readable → falls through (observe parity)
|
|
276
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
test("scope-plan: unreadable namespace override falls through to coding/legacy", () => {
|
|
279
|
+
// No policy for "restricted" → canReadNamespace(default, "restricted") → false.
|
|
280
|
+
const config = withSelfPolicy(baseConfig(), "alice");
|
|
281
|
+
const plan = resolveScopePlan({
|
|
282
|
+
config,
|
|
283
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
284
|
+
sessionKey: "alice:sess-1",
|
|
285
|
+
namespace: "restricted",
|
|
286
|
+
codingContext: codingContext("myproj"),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Unreadable override → namespaceOverride is undefined in the plan.
|
|
290
|
+
assert.equal(plan.namespaceOverride, undefined);
|
|
291
|
+
// Falls through to coding overlay (alice has a policy + coding context).
|
|
292
|
+
assert.notEqual(plan.codingOverlay, null);
|
|
293
|
+
assert.notEqual(plan.baseNamespace, "restricted");
|
|
294
|
+
assert.notEqual(plan.baseNamespace, "alice", "base should be the overlaid namespace");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
298
|
+
// Snapshot 10: defaultRecallNamespaces omits self → overlay LCM collapses
|
|
299
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
test("scope-plan: self not in defaultRecallNamespaces → LCM collapses to default", () => {
|
|
302
|
+
const config = withSelfPolicy(
|
|
303
|
+
baseConfig({
|
|
304
|
+
defaultRecallNamespaces: ["shared"],
|
|
305
|
+
} as Partial<PluginConfig>),
|
|
306
|
+
"alice",
|
|
307
|
+
);
|
|
308
|
+
const plan = resolveScopePlan({
|
|
309
|
+
config,
|
|
310
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
311
|
+
sessionKey: "alice:sess-1",
|
|
312
|
+
codingContext: codingContext("myproj"),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Coding overlay IS resolved (coding context + projectScope).
|
|
316
|
+
assert.notEqual(plan.codingOverlay, null);
|
|
317
|
+
// codingOverlaySelfReadable = false (self "alice" not in readable set).
|
|
318
|
+
// LCM collapses to default.
|
|
319
|
+
assert.deepEqual(plan.lcmReadNamespaces, ["default"]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
323
|
+
// Cross-check: resolveScopePlan is pure (same inputs → same outputs)
|
|
324
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
test("scope-plan: resolver is pure — identical inputs produce identical plans", () => {
|
|
327
|
+
const config = withSelfPolicy(baseConfig(), "alice");
|
|
328
|
+
const ctx = codingContext("myproj");
|
|
329
|
+
const opts = { config, namespacesEnabled: config.namespacesEnabled, sessionKey: "alice:sess-1", codingContext: ctx } as const;
|
|
330
|
+
|
|
331
|
+
const a = resolveScopePlan(opts);
|
|
332
|
+
const b = resolveScopePlan(opts);
|
|
333
|
+
|
|
334
|
+
assert.deepEqual(a.readNamespaces, b.readNamespaces);
|
|
335
|
+
assert.deepEqual(a.lcmReadNamespaces, b.lcmReadNamespaces);
|
|
336
|
+
assert.deepEqual([...a.lcmReadSessionIds], [...b.lcmReadSessionIds]);
|
|
337
|
+
assert.equal(a.baseNamespace, b.baseNamespace);
|
|
338
|
+
assert.equal(a.codingOverlay?.namespace, b.codingOverlay?.namespace);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
342
|
+
// Parity invariant: LCM keys derived through lcmSessionKeyForNamespace
|
|
343
|
+
// (rule 22 — never hardcoded `:`-joins)
|
|
344
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
test("scope-plan: LCM session ids match lcmSessionKeyForNamespace encoding", () => {
|
|
347
|
+
const config = withSelfPolicy(baseConfig(), "alice");
|
|
348
|
+
const plan = resolveScopePlan({
|
|
349
|
+
config,
|
|
350
|
+
namespacesEnabled: config.namespacesEnabled,
|
|
351
|
+
sessionKey: "alice:sess-1",
|
|
352
|
+
codingContext: codingContext("myproj"),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Each LCM read session id must equal lcmSessionKeyForNamespace(ns, sk, default).
|
|
356
|
+
const expected = plan.lcmReadNamespaces.map(
|
|
357
|
+
(ns) => lcmSessionKeyForNamespace(ns, "alice:sess-1", "default") ?? "alice:sess-1",
|
|
358
|
+
);
|
|
359
|
+
assert.deepEqual([...plan.lcmReadSessionIds], expected);
|
|
360
|
+
});
|