@remnic/core 9.69.4 → 9.69.6
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-admin-ops-surface.js +20 -20
- package/dist/access-authorization-probe.js +21 -21
- package/dist/access-boundary.js +20 -20
- package/dist/access-cli.js +71 -71
- package/dist/access-extraction-force-flush.js +20 -20
- package/dist/access-http-query.js +22 -22
- package/dist/access-http.js +36 -36
- package/dist/access-identity-continuity-surface.js +20 -20
- package/dist/access-lcm-surface.js +20 -20
- package/dist/access-mcp.js +29 -29
- package/dist/access-namespace-preflight.js +20 -20
- package/dist/access-observe-write-surface.js +20 -20
- package/dist/access-operations-batch.js +23 -23
- package/dist/access-operations.js +28 -28
- package/dist/access-recall-concurrency.js +20 -20
- package/dist/access-recall-response.js +20 -20
- package/dist/access-recall-surface.js +20 -20
- package/dist/access-schema.js +4 -4
- package/dist/access-service.js +20 -20
- package/dist/access-surface-catalog.js +22 -22
- package/dist/active-recall.js +2 -2
- package/dist/briefing.js +8 -8
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
- package/dist/causal-consolidation.js +9 -9
- package/dist/{chunk-N3VPKMU3.js → chunk-3X7Z467X.js} +2 -2
- package/dist/{chunk-EB3V7PJC.js → chunk-4OH6FGAO.js} +13 -13
- package/dist/{chunk-YSD4MK5Q.js → chunk-5HP6LVWU.js} +2 -2
- package/dist/{chunk-OM4VYQPH.js → chunk-6EXRODDF.js} +2 -2
- package/dist/{chunk-2L5FSNPC.js → chunk-6F5WBXE2.js} +4 -4
- package/dist/{chunk-DZPX5WSG.js → chunk-6JYTP3JP.js} +3 -3
- package/dist/{chunk-RFYKLES2.js → chunk-6KQLDPKA.js} +2 -2
- package/dist/{chunk-J2R275FN.js → chunk-7DXW5V2W.js} +2 -2
- package/dist/chunk-7O5EW7KX.js +27 -0
- package/dist/chunk-7O5EW7KX.js.map +1 -0
- package/dist/{chunk-227UFSEQ.js → chunk-7RZJWZ46.js} +5 -5
- package/dist/{chunk-3KURGLO7.js → chunk-7VA7PARC.js} +2 -2
- package/dist/{chunk-TOUZM2RQ.js → chunk-AJFKQJQK.js} +2 -2
- package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-WZBDOI2C.js → chunk-C5FRCM4B.js} +2 -2
- package/dist/{chunk-PRCJDCDD.js → chunk-CGFDT752.js} +4 -4
- package/dist/{chunk-V63BBOFS.js → chunk-DD2BKHSD.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
- package/dist/{chunk-WJTB4SF5.js → chunk-EAG7YMIS.js} +2 -2
- package/dist/{chunk-3HEYRDNF.js → chunk-EJJ57PTY.js} +2 -2
- package/dist/{chunk-YHSZTMM3.js → chunk-EVLHZ6F3.js} +2 -2
- package/dist/{chunk-35J5ABJC.js → chunk-FGV7V2N4.js} +2 -2
- package/dist/{chunk-6AYPXHOF.js → chunk-GM35IHF7.js} +2 -2
- package/dist/{chunk-MU4FAKGV.js → chunk-HDEQRI2O.js} +2 -2
- package/dist/{chunk-PBSZQBFM.js → chunk-IFHT77WK.js} +2 -2
- package/dist/{chunk-OZFHE6FY.js → chunk-IHFN3JTR.js} +3 -3
- package/dist/{chunk-LXMYIJOO.js → chunk-ISDSX7GW.js} +1 -1
- package/dist/{chunk-EZPBQVGZ.js → chunk-JJAOVJ3E.js} +5 -5
- package/dist/{chunk-VSWRJQQW.js → chunk-KJUWWN3T.js} +2 -1
- package/dist/{chunk-76X3F4WV.js → chunk-LGVIKA7O.js} +6 -6
- package/dist/{chunk-V3KLIYTW.js → chunk-M4MNLPO7.js} +2 -2
- package/dist/{chunk-7DQQYJKU.js → chunk-MDQXW235.js} +2 -2
- package/dist/{chunk-ASDK4GEV.js → chunk-MOUCCUET.js} +2 -2
- package/dist/{chunk-DT7LKLET.js → chunk-MVH7YAWY.js} +2 -2
- package/dist/{chunk-GH477IGR.js → chunk-NAB4MKTJ.js} +3 -3
- package/dist/{chunk-FGORNFTZ.js → chunk-NM5EOKN7.js} +2 -2
- package/dist/{chunk-LXBVTRBG.js → chunk-OUAK37ID.js} +2 -2
- package/dist/{chunk-HCERYDRI.js → chunk-PGD3DXZP.js} +2 -2
- package/dist/{chunk-6CT4HSVJ.js → chunk-PY5E3UOD.js} +2 -2
- package/dist/{chunk-YNF4ZT3X.js → chunk-QA6H4PIO.js} +2 -2
- package/dist/{chunk-EJCKJS4X.js → chunk-R3YVNAJB.js} +2 -2
- package/dist/{chunk-XVI2TR26.js → chunk-R4FPBSFD.js} +2 -2
- package/dist/{chunk-VVE5SLO5.js → chunk-SKVSBTFQ.js} +2 -2
- package/dist/{chunk-S7363OIV.js → chunk-SZ3SRRYZ.js} +2 -2
- package/dist/{chunk-WRPRUMUI.js → chunk-SZZGVXKC.js} +2 -2
- package/dist/{chunk-TFN3PPYO.js → chunk-TGDFKC7X.js} +2 -2
- package/dist/{chunk-7SL6EJLT.js → chunk-TP3N5364.js} +2 -2
- package/dist/{chunk-IPLYGWQF.js → chunk-VDX2J7OX.js} +6 -6
- package/dist/{chunk-VJERIMIX.js → chunk-W6V2XVUO.js} +3 -3
- package/dist/{chunk-DA2XDNS5.js → chunk-WAPSYHNR.js} +8 -8
- package/dist/{chunk-5FBA6UXN.js → chunk-WTWHORVW.js} +7 -7
- package/dist/{chunk-HBOQO4GB.js → chunk-X3DTN2RC.js} +8 -8
- package/dist/{chunk-UFY4G6EC.js → chunk-XOGYSWQZ.js} +10 -10
- package/dist/{chunk-J6SWAE74.js → chunk-YERBUSU2.js} +2 -2
- package/dist/{chunk-D65XGENZ.js → chunk-YPNTBYEV.js} +3 -3
- package/dist/{chunk-WOA7DS7O.js → chunk-YTCE2JQL.js} +251 -48
- package/dist/chunk-YTCE2JQL.js.map +1 -0
- package/dist/{chunk-FNNWK7G7.js → chunk-ZET6A3OF.js} +2 -2
- package/dist/{chunk-EA2C5Z7V.js → chunk-ZIZUB6VB.js} +28 -28
- package/dist/cli.js +54 -54
- package/dist/coding/export-okf-codegraph.js +8 -8
- package/dist/compounding/engine.js +8 -8
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.js +8 -8
- package/dist/connectors/index.js +8 -8
- package/dist/convergence-refresh.js +10 -10
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +10 -10
- package/dist/entity-retrieval.js +8 -8
- package/dist/external-wiki-access.js +21 -21
- package/dist/extraction.js +8 -8
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +89 -1
- package/dist/index.js +106 -94
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/location/index.js +10 -10
- package/dist/maintenance/memory-governance.js +8 -8
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -8
- package/dist/maintenance/rebuild-memory-projection.js +9 -9
- package/dist/namespaces/migrate.js +9 -9
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +8 -8
- package/dist/operator-doctor-corpus.js +11 -11
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/maintenance.js +17 -17
- package/dist/orchestrator.js +71 -71
- package/dist/recall-navigate-link.d.ts +9 -3
- package/dist/recall-navigate-link.js +3 -15
- package/dist/recall-navigate-link.js.map +1 -1
- package/dist/recall-navigate-traverse.d.ts +21 -0
- package/dist/recall-navigate-traverse.js +44 -0
- package/dist/recall-navigate-traverse.js.map +1 -0
- package/dist/resume-bundles.js +3 -3
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +9 -9
- package/dist/semantic-rule-promotion.js +8 -8
- package/dist/semantic-rule-verifier.js +8 -8
- package/dist/source-agent-qualifier.js +8 -8
- package/dist/standup/build.js +8 -8
- package/dist/storage.js +7 -7
- package/dist/support-passport/index.js +23 -23
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +9 -9
- package/dist/verified-recall.js +8 -8
- package/dist/who-knows-cli.js +9 -9
- package/dist/who-knows.js +8 -8
- package/package.json +2 -2
- package/src/activity/index.ts +3 -0
- package/src/activity/journal-vault-provenance.test.ts +120 -0
- package/src/activity/journal-vault-provenance.ts +67 -0
- package/src/activity/privacy-gate-resolve.test.ts +175 -0
- package/src/activity/privacy-gate-resolve.ts +84 -0
- package/src/activity/timeline/index.ts +1 -0
- package/src/activity/timeline/recap-fallback.test.ts +184 -0
- package/src/activity/timeline/recap-fallback.ts +106 -0
- package/src/activity/vault-dry-run.test.ts +114 -0
- package/src/activity/vault-dry-run.ts +76 -0
- package/src/recall-navigate-link.test.ts +28 -6
- package/src/recall-navigate-link.ts +16 -2
- package/src/recall-navigate-traverse.test.ts +247 -0
- package/src/recall-navigate-traverse.ts +73 -0
- package/dist/chunk-WOA7DS7O.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
- /package/dist/{chunk-N3VPKMU3.js.map → chunk-3X7Z467X.js.map} +0 -0
- /package/dist/{chunk-EB3V7PJC.js.map → chunk-4OH6FGAO.js.map} +0 -0
- /package/dist/{chunk-YSD4MK5Q.js.map → chunk-5HP6LVWU.js.map} +0 -0
- /package/dist/{chunk-OM4VYQPH.js.map → chunk-6EXRODDF.js.map} +0 -0
- /package/dist/{chunk-2L5FSNPC.js.map → chunk-6F5WBXE2.js.map} +0 -0
- /package/dist/{chunk-DZPX5WSG.js.map → chunk-6JYTP3JP.js.map} +0 -0
- /package/dist/{chunk-RFYKLES2.js.map → chunk-6KQLDPKA.js.map} +0 -0
- /package/dist/{chunk-J2R275FN.js.map → chunk-7DXW5V2W.js.map} +0 -0
- /package/dist/{chunk-227UFSEQ.js.map → chunk-7RZJWZ46.js.map} +0 -0
- /package/dist/{chunk-3KURGLO7.js.map → chunk-7VA7PARC.js.map} +0 -0
- /package/dist/{chunk-TOUZM2RQ.js.map → chunk-AJFKQJQK.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-WZBDOI2C.js.map → chunk-C5FRCM4B.js.map} +0 -0
- /package/dist/{chunk-PRCJDCDD.js.map → chunk-CGFDT752.js.map} +0 -0
- /package/dist/{chunk-V63BBOFS.js.map → chunk-DD2BKHSD.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
- /package/dist/{chunk-WJTB4SF5.js.map → chunk-EAG7YMIS.js.map} +0 -0
- /package/dist/{chunk-3HEYRDNF.js.map → chunk-EJJ57PTY.js.map} +0 -0
- /package/dist/{chunk-YHSZTMM3.js.map → chunk-EVLHZ6F3.js.map} +0 -0
- /package/dist/{chunk-35J5ABJC.js.map → chunk-FGV7V2N4.js.map} +0 -0
- /package/dist/{chunk-6AYPXHOF.js.map → chunk-GM35IHF7.js.map} +0 -0
- /package/dist/{chunk-MU4FAKGV.js.map → chunk-HDEQRI2O.js.map} +0 -0
- /package/dist/{chunk-PBSZQBFM.js.map → chunk-IFHT77WK.js.map} +0 -0
- /package/dist/{chunk-OZFHE6FY.js.map → chunk-IHFN3JTR.js.map} +0 -0
- /package/dist/{chunk-LXMYIJOO.js.map → chunk-ISDSX7GW.js.map} +0 -0
- /package/dist/{chunk-EZPBQVGZ.js.map → chunk-JJAOVJ3E.js.map} +0 -0
- /package/dist/{chunk-VSWRJQQW.js.map → chunk-KJUWWN3T.js.map} +0 -0
- /package/dist/{chunk-76X3F4WV.js.map → chunk-LGVIKA7O.js.map} +0 -0
- /package/dist/{chunk-V3KLIYTW.js.map → chunk-M4MNLPO7.js.map} +0 -0
- /package/dist/{chunk-7DQQYJKU.js.map → chunk-MDQXW235.js.map} +0 -0
- /package/dist/{chunk-ASDK4GEV.js.map → chunk-MOUCCUET.js.map} +0 -0
- /package/dist/{chunk-DT7LKLET.js.map → chunk-MVH7YAWY.js.map} +0 -0
- /package/dist/{chunk-GH477IGR.js.map → chunk-NAB4MKTJ.js.map} +0 -0
- /package/dist/{chunk-FGORNFTZ.js.map → chunk-NM5EOKN7.js.map} +0 -0
- /package/dist/{chunk-LXBVTRBG.js.map → chunk-OUAK37ID.js.map} +0 -0
- /package/dist/{chunk-HCERYDRI.js.map → chunk-PGD3DXZP.js.map} +0 -0
- /package/dist/{chunk-6CT4HSVJ.js.map → chunk-PY5E3UOD.js.map} +0 -0
- /package/dist/{chunk-YNF4ZT3X.js.map → chunk-QA6H4PIO.js.map} +0 -0
- /package/dist/{chunk-EJCKJS4X.js.map → chunk-R3YVNAJB.js.map} +0 -0
- /package/dist/{chunk-XVI2TR26.js.map → chunk-R4FPBSFD.js.map} +0 -0
- /package/dist/{chunk-VVE5SLO5.js.map → chunk-SKVSBTFQ.js.map} +0 -0
- /package/dist/{chunk-S7363OIV.js.map → chunk-SZ3SRRYZ.js.map} +0 -0
- /package/dist/{chunk-WRPRUMUI.js.map → chunk-SZZGVXKC.js.map} +0 -0
- /package/dist/{chunk-TFN3PPYO.js.map → chunk-TGDFKC7X.js.map} +0 -0
- /package/dist/{chunk-7SL6EJLT.js.map → chunk-TP3N5364.js.map} +0 -0
- /package/dist/{chunk-IPLYGWQF.js.map → chunk-VDX2J7OX.js.map} +0 -0
- /package/dist/{chunk-VJERIMIX.js.map → chunk-W6V2XVUO.js.map} +0 -0
- /package/dist/{chunk-DA2XDNS5.js.map → chunk-WAPSYHNR.js.map} +0 -0
- /package/dist/{chunk-5FBA6UXN.js.map → chunk-WTWHORVW.js.map} +0 -0
- /package/dist/{chunk-HBOQO4GB.js.map → chunk-X3DTN2RC.js.map} +0 -0
- /package/dist/{chunk-UFY4G6EC.js.map → chunk-XOGYSWQZ.js.map} +0 -0
- /package/dist/{chunk-J6SWAE74.js.map → chunk-YERBUSU2.js.map} +0 -0
- /package/dist/{chunk-D65XGENZ.js.map → chunk-YPNTBYEV.js.map} +0 -0
- /package/dist/{chunk-FNNWK7G7.js.map → chunk-ZET6A3OF.js.map} +0 -0
- /package/dist/{chunk-EA2C5Z7V.js.map → chunk-ZIZUB6VB.js.map} +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure vault publish dry-run planner (issue #1985).
|
|
3
|
+
*
|
|
4
|
+
* `planVaultDryRun` predicts per-note publish outcomes from text the caller
|
|
5
|
+
* has already read. It takes strings, not paths, and returns only a report:
|
|
6
|
+
* zero writes is a property of the signature, not a promise. Reporting a
|
|
7
|
+
* note as `skipped` with reason `missing_file` when `currentText` is null,
|
|
8
|
+
* matching the real publisher, which refuses to create a missing note.
|
|
9
|
+
*
|
|
10
|
+
* Internal helper: nothing calls it yet. CLI/HTTP wiring for the `--dry-run`
|
|
11
|
+
* flag is a later slice of #1985.
|
|
12
|
+
*/
|
|
13
|
+
import { summarizeVaultPublish } from "./vault-status.js";
|
|
14
|
+
import type { VaultPublishOutcome, VaultPublishResult, VaultPublishStatus } from "./vault-status.js";
|
|
15
|
+
|
|
16
|
+
export interface VaultDryRunInput {
|
|
17
|
+
/** Vault-relative note path, for reporting only. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** Current note text, or null when the note does not exist. */
|
|
20
|
+
currentText: string | null;
|
|
21
|
+
/** The text a real publish would produce, or null when it would skip. */
|
|
22
|
+
nextText: string | null;
|
|
23
|
+
/** Required when nextText is null: why the publish would skip. */
|
|
24
|
+
skipReason?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function planVaultDryRun(inputs: readonly VaultDryRunInput[]): VaultPublishStatus {
|
|
28
|
+
const results: VaultPublishResult[] = [];
|
|
29
|
+
for (const input of inputs) {
|
|
30
|
+
if (typeof input !== "object" || input === null) {
|
|
31
|
+
throw new TypeError(`vault dry-run inputs must be objects (got ${typeof input})`);
|
|
32
|
+
}
|
|
33
|
+
if (typeof input.path !== "string" || input.path.trim() === "") {
|
|
34
|
+
throw new RangeError(`vault dry-run input requires a non-blank path (got ${JSON.stringify(input.path)})`);
|
|
35
|
+
}
|
|
36
|
+
// Validate the exact values before classification: a non-string or
|
|
37
|
+
// undefined text field is malformed input, and letting it reach the
|
|
38
|
+
// comparison would silently report a prediction for data the contract
|
|
39
|
+
// does not permit (two undefined fields would read as "unchanged").
|
|
40
|
+
for (const field of ["currentText", "nextText"] as const) {
|
|
41
|
+
const value = input[field];
|
|
42
|
+
if (value !== null && typeof value !== "string") {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
`vault dry-run input ${field} must be a string or null (got ${typeof value})`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (input.nextText === null) {
|
|
49
|
+
if (typeof input.skipReason !== "string" || input.skipReason.trim() === "") {
|
|
50
|
+
throw new TypeError(
|
|
51
|
+
`vault dry-run skip (nextText null) requires a non-blank skipReason (got ${JSON.stringify(input.skipReason)})`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
results.push({ path: input.path, outcome: "skipped", reason: input.skipReason });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (input.skipReason !== undefined) {
|
|
58
|
+
throw new TypeError(
|
|
59
|
+
`vault dry-run skipReason is only valid when nextText is null (path ${JSON.stringify(input.path)})`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
// A missing note is NOT a create: the real publisher (vault-publish.ts
|
|
63
|
+
// publishVaultRegion) refuses to create missing files and returns
|
|
64
|
+
// missing_file, so the dry run must predict that skip rather than promise
|
|
65
|
+
// an update the real run will not perform.
|
|
66
|
+
if (input.currentText === null) {
|
|
67
|
+
results.push({ path: input.path, outcome: "skipped", reason: "missing_file" });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Byte-exact compare: a publish that rewrites only trailing whitespace is
|
|
71
|
+
// still a write.
|
|
72
|
+
const outcome: VaultPublishOutcome = input.currentText === input.nextText ? "unchanged" : "updated";
|
|
73
|
+
results.push({ path: input.path, outcome });
|
|
74
|
+
}
|
|
75
|
+
return summarizeVaultPublish(results);
|
|
76
|
+
}
|
|
@@ -2,18 +2,40 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
4
|
import { parseNavigateLinkType } from "./recall-navigate-link.js";
|
|
5
|
+
import { selectTraverseNeighbors } from "./recall-navigate-traverse.js";
|
|
5
6
|
|
|
6
|
-
test("
|
|
7
|
-
for (const type of ["supports", "contradicts", "elaborates", "causes", "caused_by"]
|
|
7
|
+
test("every navigation relation parses", () => {
|
|
8
|
+
for (const type of ["supports", "contradicts", "elaborates", "causes", "caused_by"]) {
|
|
8
9
|
assert.deepEqual(parseNavigateLinkType(type), { ok: true, type });
|
|
9
10
|
}
|
|
10
11
|
});
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Persisted MemoryLinkType values must parse too: real frontmatter carries
|
|
14
|
+
// them, and a traversal over stored links loses those neighbors otherwise.
|
|
15
|
+
test("persisted MemoryLinkType values parse", () => {
|
|
16
|
+
for (const type of ["follows", "references", "related"]) {
|
|
17
|
+
assert.deepEqual(parseNavigateLinkType(type), { ok: true, type });
|
|
18
|
+
}
|
|
15
19
|
});
|
|
16
20
|
|
|
17
|
-
test("empty link
|
|
21
|
+
test("unknown or empty link types are refused", () => {
|
|
22
|
+
assert.deepEqual(parseNavigateLinkType("supersedesX"), { ok: false, error: "unknown_link" });
|
|
18
23
|
assert.deepEqual(parseNavigateLinkType(""), { ok: false, error: "unknown_link" });
|
|
24
|
+
assert.deepEqual(parseNavigateLinkType("Supports"), { ok: false, error: "unknown_link" });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// Round 3: the stepper's navigation contract accepts supersedes, so the
|
|
29
|
+
// shared selector vocabulary must too.
|
|
30
|
+
test("supersedes parses and filters", () => {
|
|
31
|
+
assert.deepEqual(parseNavigateLinkType("supersedes"), { ok: true, type: "supersedes" });
|
|
32
|
+
const result = selectTraverseNeighbors({
|
|
33
|
+
links: [
|
|
34
|
+
{ targetId: "m-1", linkType: "supersedes" },
|
|
35
|
+
{ targetId: "m-2", linkType: "supports" },
|
|
36
|
+
],
|
|
37
|
+
relation: "supersedes",
|
|
38
|
+
});
|
|
39
|
+
assert.ok(result.ok);
|
|
40
|
+
assert.deepEqual(result.neighbors, [{ targetId: "m-1", linkType: "supersedes" }]);
|
|
19
41
|
});
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recall navigation link-type parse (issue #1956
|
|
2
|
+
* Recall navigation link-type parse (issue #1956).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* One allow-list covering BOTH vocabularies a traversal can meet:
|
|
5
|
+
* the navigation set this module originally defined and the persisted
|
|
6
|
+
* `MemoryLinkType` values (`follows | references | related`) that real
|
|
7
|
+
* memory frontmatter carries. Review found the narrower list silently
|
|
8
|
+
* dropped every persisted `follows`/`references`/`related` edge and
|
|
9
|
+
* rejected `relation: "related"` as unknown — a traversal over actual
|
|
10
|
+
* stored links would lose valid neighbors. Surfaces wait.
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
const NAVIGATE_LINK_TYPES = [
|
|
@@ -10,6 +16,14 @@ const NAVIGATE_LINK_TYPES = [
|
|
|
10
16
|
"elaborates",
|
|
11
17
|
"causes",
|
|
12
18
|
"caused_by",
|
|
19
|
+
// Persisted MemoryLinkType values (types.ts) that real frontmatter carries.
|
|
20
|
+
"follows",
|
|
21
|
+
"references",
|
|
22
|
+
"related",
|
|
23
|
+
// The stepper's own navigation contract (recall-navigate.ts) accepts
|
|
24
|
+
// supersedes; omitting it here would reject a relation that surface is
|
|
25
|
+
// prepared to handle.
|
|
26
|
+
"supersedes",
|
|
13
27
|
] as const;
|
|
14
28
|
|
|
15
29
|
export type NavigateLinkType = (typeof NAVIGATE_LINK_TYPES)[number];
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_TRAVERSE_LIMIT } from "./recall-navigate-traverse.js";
|
|
5
|
+
import { selectTraverseNeighbors } from "./recall-navigate-traverse.js";
|
|
6
|
+
|
|
7
|
+
test("no relation returns all known-type links sorted", () => {
|
|
8
|
+
const result = selectTraverseNeighbors({
|
|
9
|
+
links: [
|
|
10
|
+
{ targetId: "m-3", linkType: "supports" },
|
|
11
|
+
{ targetId: "m-1", linkType: "elaborates" },
|
|
12
|
+
{ targetId: "m-2", linkType: "causes" },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
assert.ok(result.ok);
|
|
16
|
+
assert.equal(result.truncated, false);
|
|
17
|
+
assert.deepEqual(result.neighbors, [
|
|
18
|
+
{ targetId: "m-2", linkType: "causes" },
|
|
19
|
+
{ targetId: "m-1", linkType: "elaborates" },
|
|
20
|
+
{ targetId: "m-3", linkType: "supports" },
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("relation filter returns only that relation", () => {
|
|
25
|
+
const result = selectTraverseNeighbors({
|
|
26
|
+
links: [
|
|
27
|
+
{ targetId: "m-1", linkType: "supports" },
|
|
28
|
+
{ targetId: "m-2", linkType: "contradicts" },
|
|
29
|
+
{ targetId: "m-3", linkType: "supports" },
|
|
30
|
+
],
|
|
31
|
+
relation: "supports",
|
|
32
|
+
});
|
|
33
|
+
assert.ok(result.ok);
|
|
34
|
+
assert.deepEqual(result.neighbors, [
|
|
35
|
+
{ targetId: "m-1", linkType: "supports" },
|
|
36
|
+
{ targetId: "m-3", linkType: "supports" },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("unknown relation is refused with unknown_relation", () => {
|
|
41
|
+
const result = selectTraverseNeighbors({
|
|
42
|
+
links: [{ targetId: "m-1", linkType: "supports" }],
|
|
43
|
+
relation: "destroys",
|
|
44
|
+
});
|
|
45
|
+
assert.deepEqual(result, { ok: false, error: "unknown_relation" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("unknown linkType link is skipped while siblings survive", () => {
|
|
49
|
+
const result = selectTraverseNeighbors({
|
|
50
|
+
links: [
|
|
51
|
+
{ targetId: "m-1", linkType: "hyperlinks" },
|
|
52
|
+
{ targetId: "m-2", linkType: "supports" },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
assert.ok(result.ok);
|
|
56
|
+
assert.deepEqual(result.neighbors, [{ targetId: "m-2", linkType: "supports" }]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("duplicate targetIds collapse to the first occurrence", () => {
|
|
60
|
+
const result = selectTraverseNeighbors({
|
|
61
|
+
links: [
|
|
62
|
+
{ targetId: "m-1", linkType: "causes" },
|
|
63
|
+
{ targetId: "m-1", linkType: "supports" },
|
|
64
|
+
{ targetId: "m-2", linkType: "elaborates" },
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
assert.ok(result.ok);
|
|
68
|
+
assert.deepEqual(result.neighbors, [
|
|
69
|
+
{ targetId: "m-1", linkType: "causes" },
|
|
70
|
+
{ targetId: "m-2", linkType: "elaborates" },
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("blank targetId is skipped", () => {
|
|
75
|
+
const result = selectTraverseNeighbors({
|
|
76
|
+
links: [
|
|
77
|
+
{ targetId: "", linkType: "supports" },
|
|
78
|
+
{ targetId: " ", linkType: "supports" },
|
|
79
|
+
{ targetId: "m-1", linkType: "supports" },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
assert.ok(result.ok);
|
|
83
|
+
assert.deepEqual(result.neighbors, [{ targetId: "m-1", linkType: "supports" }]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("limit truncates after sorting and sets truncated", () => {
|
|
87
|
+
const result = selectTraverseNeighbors({
|
|
88
|
+
links: [
|
|
89
|
+
{ targetId: "m-9", linkType: "supports" },
|
|
90
|
+
{ targetId: "m-2", linkType: "causes" },
|
|
91
|
+
{ targetId: "m-5", linkType: "elaborates" },
|
|
92
|
+
],
|
|
93
|
+
limit: 2,
|
|
94
|
+
});
|
|
95
|
+
assert.ok(result.ok);
|
|
96
|
+
assert.equal(result.truncated, true);
|
|
97
|
+
assert.deepEqual(result.neighbors, [
|
|
98
|
+
{ targetId: "m-2", linkType: "causes" },
|
|
99
|
+
{ targetId: "m-5", linkType: "elaborates" },
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("default limit caps at DEFAULT_TRAVERSE_LIMIT", () => {
|
|
104
|
+
const links = Array.from({ length: DEFAULT_TRAVERSE_LIMIT + 3 }, (_, i) => ({
|
|
105
|
+
targetId: `m-${i}`,
|
|
106
|
+
linkType: "supports",
|
|
107
|
+
}));
|
|
108
|
+
const result = selectTraverseNeighbors({ links });
|
|
109
|
+
assert.ok(result.ok);
|
|
110
|
+
assert.equal(result.truncated, true);
|
|
111
|
+
assert.equal(result.neighbors.length, DEFAULT_TRAVERSE_LIMIT);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("limit 0 is invalid_limit", () => {
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
selectTraverseNeighbors({ links: [], limit: 0 }),
|
|
117
|
+
{ ok: false, error: "invalid_limit" },
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("limit -1 is invalid_limit", () => {
|
|
122
|
+
assert.deepEqual(
|
|
123
|
+
selectTraverseNeighbors({ links: [], limit: -1 }),
|
|
124
|
+
{ ok: false, error: "invalid_limit" },
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("limit 1.5 is invalid_limit", () => {
|
|
129
|
+
assert.deepEqual(
|
|
130
|
+
selectTraverseNeighbors({ links: [], limit: 1.5 }),
|
|
131
|
+
{ ok: false, error: "invalid_limit" },
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("limit NaN is invalid_limit", () => {
|
|
136
|
+
assert.deepEqual(
|
|
137
|
+
selectTraverseNeighbors({ links: [], limit: Number.NaN }),
|
|
138
|
+
{ ok: false, error: "invalid_limit" },
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("shuffled input is deep-equal to sorted-input result", () => {
|
|
143
|
+
const sorted = [
|
|
144
|
+
{ targetId: "m-2", linkType: "caused_by" },
|
|
145
|
+
{ targetId: "m-1", linkType: "causes" },
|
|
146
|
+
{ targetId: "m-3", linkType: "elaborates" },
|
|
147
|
+
{ targetId: "m-4", linkType: "supports" },
|
|
148
|
+
];
|
|
149
|
+
const result = selectTraverseNeighbors({
|
|
150
|
+
links: [...sorted].reverse(),
|
|
151
|
+
});
|
|
152
|
+
assert.ok(result.ok);
|
|
153
|
+
assert.deepEqual(result.neighbors, sorted);
|
|
154
|
+
assert.deepEqual(selectTraverseNeighbors({ links: sorted }), result);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("inputs are not mutated", () => {
|
|
158
|
+
const links = [
|
|
159
|
+
{ targetId: "m-2", linkType: "supports" },
|
|
160
|
+
{ targetId: "m-1", linkType: "causes" },
|
|
161
|
+
];
|
|
162
|
+
selectTraverseNeighbors({ links, relation: "causes", limit: 1 });
|
|
163
|
+
assert.deepEqual(links, [
|
|
164
|
+
{ targetId: "m-2", linkType: "supports" },
|
|
165
|
+
{ targetId: "m-1", linkType: "causes" },
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Review: the contract said bad stored data is skipped, but a nullish
|
|
170
|
+
// targetId threw before the skip checks could run.
|
|
171
|
+
test("a nullish or non-string targetId is skipped, never thrown", () => {
|
|
172
|
+
const result = selectTraverseNeighbors({
|
|
173
|
+
links: [
|
|
174
|
+
{ targetId: undefined as unknown as string, linkType: "supports" },
|
|
175
|
+
{ targetId: null as unknown as string, linkType: "supports" },
|
|
176
|
+
{ targetId: 42 as unknown as string, linkType: "supports" },
|
|
177
|
+
{ targetId: "m-real", linkType: "supports" },
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
assert.ok(result.ok);
|
|
181
|
+
assert.deepEqual(result.neighbors, [{ targetId: "m-real", linkType: "supports" }]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// A padded id is malformed, not a variant of the trimmed id: trimming would
|
|
185
|
+
// guess at an identity, so the row is skipped.
|
|
186
|
+
test("a whitespace-padded targetId is skipped rather than trimmed", () => {
|
|
187
|
+
const result = selectTraverseNeighbors({
|
|
188
|
+
links: [
|
|
189
|
+
{ targetId: " m-1", linkType: "supports" },
|
|
190
|
+
{ targetId: "m-2", linkType: "supports" },
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
assert.ok(result.ok);
|
|
194
|
+
assert.deepEqual(result.neighbors, [{ targetId: "m-2", linkType: "supports" }]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Review: dedup ran before sorting, so input order decided which relation
|
|
198
|
+
// survived for a doubly-linked target. Sorting first fixes the survivor.
|
|
199
|
+
test("a target under two relations keeps the smallest linkType regardless of input order", () => {
|
|
200
|
+
const a = { targetId: "m-1", linkType: "supports" };
|
|
201
|
+
const b = { targetId: "m-1", linkType: "contradicts" };
|
|
202
|
+
const forward = selectTraverseNeighbors({ links: [a, b] });
|
|
203
|
+
const reversed = selectTraverseNeighbors({ links: [b, a] });
|
|
204
|
+
assert.ok(forward.ok && reversed.ok);
|
|
205
|
+
assert.deepEqual(forward.neighbors, reversed.neighbors);
|
|
206
|
+
assert.deepEqual(forward.neighbors, [{ targetId: "m-1", linkType: "contradicts" }]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("sort-then-dedup also decides which target survives a tight limit", () => {
|
|
210
|
+
const links = [
|
|
211
|
+
{ targetId: "m-z", linkType: "supports" },
|
|
212
|
+
{ targetId: "m-a", linkType: "supports" },
|
|
213
|
+
{ targetId: "m-a", linkType: "elaborates" },
|
|
214
|
+
];
|
|
215
|
+
const shuffled = selectTraverseNeighbors({ links: [...links].reverse(), limit: 1 });
|
|
216
|
+
const straight = selectTraverseNeighbors({ links, limit: 1 });
|
|
217
|
+
assert.ok(shuffled.ok && straight.ok);
|
|
218
|
+
assert.deepEqual(shuffled.neighbors, straight.neighbors);
|
|
219
|
+
assert.deepEqual(straight.neighbors, [{ targetId: "m-a", linkType: "elaborates" }]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
// Round 2: the persisted MemoryLinkType values are real neighbors, not
|
|
224
|
+
// unknown rows to drop. The selector gets them from the widened parser.
|
|
225
|
+
test("persisted link types survive an unfiltered traversal", () => {
|
|
226
|
+
const result = selectTraverseNeighbors({
|
|
227
|
+
links: [
|
|
228
|
+
{ targetId: "m-1", linkType: "follows" },
|
|
229
|
+
{ targetId: "m-2", linkType: "references" },
|
|
230
|
+
{ targetId: "m-3", linkType: "related" },
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
assert.ok(result.ok);
|
|
234
|
+
assert.equal(result.neighbors.length, 3);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("relation: related is a valid filter, not unknown", () => {
|
|
238
|
+
const result = selectTraverseNeighbors({
|
|
239
|
+
links: [
|
|
240
|
+
{ targetId: "m-1", linkType: "related" },
|
|
241
|
+
{ targetId: "m-2", linkType: "supports" },
|
|
242
|
+
],
|
|
243
|
+
relation: "related",
|
|
244
|
+
});
|
|
245
|
+
assert.ok(result.ok);
|
|
246
|
+
assert.deepEqual(result.neighbors, [{ targetId: "m-1", linkType: "related" }]);
|
|
247
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall navigation traverse selection (issue #1956).
|
|
3
|
+
*
|
|
4
|
+
* Pure selection step for `traverseLinks(id, relation?, limit)`: given a
|
|
5
|
+
* recalled memory's links, pick which neighbors a traverse would visit.
|
|
6
|
+
* Internal; surfaces wire it in a later slice.
|
|
7
|
+
*/
|
|
8
|
+
import { parseNavigateLinkType } from "./recall-navigate-link.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_TRAVERSE_LIMIT = 10;
|
|
11
|
+
|
|
12
|
+
export interface TraverseNeighbor {
|
|
13
|
+
targetId: string;
|
|
14
|
+
linkType: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type TraverseSelectionResult =
|
|
18
|
+
| { ok: true; neighbors: TraverseNeighbor[]; truncated: boolean }
|
|
19
|
+
| { ok: false; error: "unknown_relation" | "invalid_limit" };
|
|
20
|
+
|
|
21
|
+
export function selectTraverseNeighbors(input: {
|
|
22
|
+
links: readonly TraverseNeighbor[];
|
|
23
|
+
/** Optional relation filter; omitted means every known relation. */
|
|
24
|
+
relation?: string;
|
|
25
|
+
limit?: number;
|
|
26
|
+
}): TraverseSelectionResult {
|
|
27
|
+
const limit = input.limit ?? DEFAULT_TRAVERSE_LIMIT;
|
|
28
|
+
if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) {
|
|
29
|
+
return { ok: false, error: "invalid_limit" };
|
|
30
|
+
}
|
|
31
|
+
if (input.relation !== undefined && !parseNavigateLinkType(input.relation).ok) {
|
|
32
|
+
return { ok: false, error: "unknown_relation" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const kept: TraverseNeighbor[] = [];
|
|
36
|
+
// Asymmetry is intentional: a bad request (unknown relation, invalid
|
|
37
|
+
// limit) is refused, but bad stored data is skipped — stored data may
|
|
38
|
+
// predate a type or carry a malformed id, and one bad row must not fail
|
|
39
|
+
// the whole traverse. A targetId that is not a non-blank string, including
|
|
40
|
+
// undefined/null and whitespace-only, is malformed and skipped. Padded ids
|
|
41
|
+
// are NOT trimmed into validity: an id is an exact identity, and guessing
|
|
42
|
+
// the intended target is worse than skipping the row.
|
|
43
|
+
for (const link of input.links) {
|
|
44
|
+
if (!parseNavigateLinkType(link.linkType).ok) continue;
|
|
45
|
+
if (input.relation !== undefined && link.linkType !== input.relation) continue;
|
|
46
|
+
if (typeof link.targetId !== "string" || link.targetId.trim() === "" || link.targetId.trim() !== link.targetId) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
kept.push({ targetId: link.targetId, linkType: link.linkType });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Sort BEFORE deduplicating: first-occurrence dedup on an unsorted list
|
|
53
|
+
// lets input order decide which relation survives for a target that is
|
|
54
|
+
// linked under two types, and after the cap is applied, which target is
|
|
55
|
+
// returned at all. Sorting first makes the survivor deterministic
|
|
56
|
+
// (smallest linkType wins the tie).
|
|
57
|
+
kept.sort((a, b) => {
|
|
58
|
+
if (a.linkType !== b.linkType) return a.linkType < b.linkType ? -1 : 1;
|
|
59
|
+
if (a.targetId !== b.targetId) return a.targetId < b.targetId ? -1 : 1;
|
|
60
|
+
return 0;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
const deduped: TraverseNeighbor[] = [];
|
|
65
|
+
for (const link of kept) {
|
|
66
|
+
if (seen.has(link.targetId)) continue;
|
|
67
|
+
seen.add(link.targetId);
|
|
68
|
+
deduped.push(link);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const truncated = deduped.length > limit;
|
|
72
|
+
return { ok: true, neighbors: truncated ? deduped.slice(0, limit) : deduped, truncated };
|
|
73
|
+
}
|