oh-my-second-brain 0.1.9 → 0.2.0

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 (96) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/adapters/claude-code/.claude-plugin/plugin.json +1 -1
  3. package/adapters/claude-code/skills/link/SKILL.md +87 -0
  4. package/adapters/codex/.codex-plugin/plugin.json +1 -1
  5. package/adapters/codex/skills/oms-link/SKILL.md +35 -0
  6. package/adapters/hermes/README.md +2 -0
  7. package/adapters/hermes/manifest.json +1 -1
  8. package/adapters/hermes/skills/link/SKILL.md +18 -0
  9. package/core/ontology/concepts/term.yaml +12 -0
  10. package/core/ontology/taxonomy.yaml +3 -0
  11. package/core/skills/link/SKILL.md +130 -0
  12. package/dist/capture/safe.d.ts +32 -2
  13. package/dist/capture/safe.js +223 -37
  14. package/dist/capture/safe.js.map +1 -1
  15. package/dist/cli/args.d.ts +2 -0
  16. package/dist/cli/args.js +6 -0
  17. package/dist/cli/args.js.map +1 -1
  18. package/dist/cli/global-writeback.d.ts +31 -0
  19. package/dist/cli/global-writeback.js +73 -0
  20. package/dist/cli/global-writeback.js.map +1 -0
  21. package/dist/cli/host-commands.d.ts +30 -0
  22. package/dist/cli/host-commands.js +89 -0
  23. package/dist/cli/host-commands.js.map +1 -0
  24. package/dist/cli/linkify.d.ts +23 -0
  25. package/dist/cli/linkify.js +118 -0
  26. package/dist/cli/linkify.js.map +1 -0
  27. package/dist/cli/oms.d.ts +1 -0
  28. package/dist/cli/oms.js +52 -66
  29. package/dist/cli/oms.js.map +1 -1
  30. package/dist/cli/usage.js +11 -1
  31. package/dist/cli/usage.js.map +1 -1
  32. package/dist/conventions/write-protocol.d.ts +39 -0
  33. package/dist/conventions/write-protocol.js +44 -0
  34. package/dist/conventions/write-protocol.js.map +1 -0
  35. package/dist/core/runtime/assets.d.ts +6 -0
  36. package/dist/core/runtime/assets.js +27 -0
  37. package/dist/core/runtime/assets.js.map +1 -1
  38. package/dist/engine/graph/resolver.d.ts +17 -0
  39. package/dist/engine/graph/resolver.js +46 -11
  40. package/dist/engine/graph/resolver.js.map +1 -1
  41. package/dist/engine/linkify/apply.d.ts +56 -0
  42. package/dist/engine/linkify/apply.js +65 -0
  43. package/dist/engine/linkify/apply.js.map +1 -0
  44. package/dist/engine/linkify/josa.d.ts +58 -0
  45. package/dist/engine/linkify/josa.js +133 -0
  46. package/dist/engine/linkify/josa.js.map +1 -0
  47. package/dist/engine/linkify/mask.d.ts +29 -0
  48. package/dist/engine/linkify/mask.js +173 -0
  49. package/dist/engine/linkify/mask.js.map +1 -0
  50. package/dist/engine/linkify/suggest.d.ts +52 -0
  51. package/dist/engine/linkify/suggest.js +181 -0
  52. package/dist/engine/linkify/suggest.js.map +1 -0
  53. package/dist/engine/linkify/types.d.ts +68 -0
  54. package/dist/engine/linkify/types.js +12 -0
  55. package/dist/engine/linkify/types.js.map +1 -0
  56. package/dist/harness/surface-registry.d.ts +1 -0
  57. package/dist/harness/surface-registry.js +27 -0
  58. package/dist/harness/surface-registry.js.map +1 -1
  59. package/dist/harness/validation.js +30 -1
  60. package/dist/harness/validation.js.map +1 -1
  61. package/dist/install/claude-marketplace.d.ts +44 -0
  62. package/dist/install/claude-marketplace.js +89 -0
  63. package/dist/install/claude-marketplace.js.map +1 -0
  64. package/dist/install/claude.js +14 -7
  65. package/dist/install/claude.js.map +1 -1
  66. package/dist/install/codex.d.ts +15 -0
  67. package/dist/install/codex.js +15 -0
  68. package/dist/install/codex.js.map +1 -1
  69. package/dist/install/common.d.ts +11 -2
  70. package/dist/install/common.js +19 -12
  71. package/dist/install/common.js.map +1 -1
  72. package/dist/install/hermes.js +15 -15
  73. package/dist/install/hermes.js.map +1 -1
  74. package/dist/install/hosts.js +31 -15
  75. package/dist/install/hosts.js.map +1 -1
  76. package/dist/link/global-config.d.ts +22 -0
  77. package/dist/link/global-config.js +69 -0
  78. package/dist/link/global-config.js.map +1 -0
  79. package/dist/link/link.d.ts +6 -3
  80. package/dist/link/link.js +12 -2
  81. package/dist/link/link.js.map +1 -1
  82. package/dist/mcp/link-tools.d.ts +81 -0
  83. package/dist/mcp/link-tools.js +157 -0
  84. package/dist/mcp/link-tools.js.map +1 -0
  85. package/dist/mcp/server.d.ts +7 -0
  86. package/dist/mcp/server.js +107 -8
  87. package/dist/mcp/server.js.map +1 -1
  88. package/dist/mcp/update-notice.d.ts +51 -0
  89. package/dist/mcp/update-notice.js +196 -0
  90. package/dist/mcp/update-notice.js.map +1 -0
  91. package/dist/update/update.d.ts +12 -0
  92. package/dist/update/update.js +2 -2
  93. package/dist/update/update.js.map +1 -1
  94. package/docs/install.md +38 -2
  95. package/docs/release.md +109 -17
  96. package/package.json +4 -2
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Write Protocol: Admission & Acceptance contract with explicit rejection & receipt.
3
+ *
4
+ * Response Shape Contract:
5
+ * - `written` results carry `receipt` (persisted, postcondition-verified) and no `rejection`.
6
+ * - `rejected` results (and contract-fixable `ask` results) carry `rejection` and no `receipt`.
7
+ * - `inbox` results carry NEITHER (inbox is a routing plan; nothing is persisted).
8
+ * - `dryRun` results carry NO receipt (a receipt attests a persisted, postcondition-verified write).
9
+ *
10
+ * Rejection Model (gajae-code reference: error{code, message, recoverable} + remediation):
11
+ * Each rejection carries a stage (admission or acceptance), code, message, and recoverable flag
12
+ * derived exhaustively from a per-code table. This ensures adding a code without a recoverable
13
+ * entry is a compile error.
14
+ */
15
+ import type { VaultSource } from "../link/link.js";
16
+ import type { WriteMode } from "../capture/safe.js";
17
+ export type WriteStage = "admission" | "acceptance";
18
+ export type WriteTargetSource = VaultSource | "explicit";
19
+ export type WriteRejectionCode = "target-unverified" | "target-invalid" | "path-unsafe" | "note-exists" | "note-missing" | "concept-unbound" | "contract-violation" | "body-missing" | "args-invalid" | "postcondition-failed";
20
+ export interface WriteRejection {
21
+ stage: WriteStage;
22
+ code: WriteRejectionCode;
23
+ message: string;
24
+ recoverable: boolean;
25
+ remediation: string;
26
+ }
27
+ export interface WriteReceipt {
28
+ resolvedVault: string;
29
+ resolutionSource: WriteTargetSource;
30
+ notePath: string;
31
+ mode: WriteMode;
32
+ concept: string | null;
33
+ postconditionVerified: boolean;
34
+ }
35
+ /**
36
+ * Create a WriteRejection with stage, code, message, and remediation.
37
+ * The recoverable flag is derived exhaustively from the per-code table.
38
+ */
39
+ export declare function rejection(stage: WriteStage, code: WriteRejectionCode, message: string, remediation: string): WriteRejection;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Write Protocol: Admission & Acceptance contract with explicit rejection & receipt.
3
+ *
4
+ * Response Shape Contract:
5
+ * - `written` results carry `receipt` (persisted, postcondition-verified) and no `rejection`.
6
+ * - `rejected` results (and contract-fixable `ask` results) carry `rejection` and no `receipt`.
7
+ * - `inbox` results carry NEITHER (inbox is a routing plan; nothing is persisted).
8
+ * - `dryRun` results carry NO receipt (a receipt attests a persisted, postcondition-verified write).
9
+ *
10
+ * Rejection Model (gajae-code reference: error{code, message, recoverable} + remediation):
11
+ * Each rejection carries a stage (admission or acceptance), code, message, and recoverable flag
12
+ * derived exhaustively from a per-code table. This ensures adding a code without a recoverable
13
+ * entry is a compile error.
14
+ */
15
+ /**
16
+ * Exhaustive recovery table: maps each rejection code to its recoverable flag.
17
+ * Adding a code without an entry is a compile error.
18
+ */
19
+ const RECOVERABLE_BY_CODE = {
20
+ "target-unverified": true,
21
+ "contract-violation": true,
22
+ "body-missing": true,
23
+ "args-invalid": true,
24
+ "path-unsafe": true,
25
+ "note-exists": false,
26
+ "note-missing": false,
27
+ "concept-unbound": false,
28
+ "target-invalid": false,
29
+ "postcondition-failed": false,
30
+ };
31
+ /**
32
+ * Create a WriteRejection with stage, code, message, and remediation.
33
+ * The recoverable flag is derived exhaustively from the per-code table.
34
+ */
35
+ export function rejection(stage, code, message, remediation) {
36
+ return {
37
+ stage,
38
+ code,
39
+ message,
40
+ remediation,
41
+ recoverable: RECOVERABLE_BY_CODE[code],
42
+ };
43
+ }
44
+ //# sourceMappingURL=write-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-protocol.js","sourceRoot":"","sources":["../../src/conventions/write-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAsCH;;;GAGG;AACH,MAAM,mBAAmB,GAAwC;IAC/D,mBAAmB,EAAE,IAAI;IACzB,oBAAoB,EAAE,IAAI;IAC1B,cAAc,EAAE,IAAI;IACpB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,IAAI;IACnB,aAAa,EAAE,KAAK;IACpB,cAAc,EAAE,KAAK;IACrB,iBAAiB,EAAE,KAAK;IACxB,gBAAgB,EAAE,KAAK;IACvB,sBAAsB,EAAE,KAAK;CAC9B,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,SAAS,CACvB,KAAiB,EACjB,IAAwB,EACxB,OAAe,EACf,WAAmB;IAEnB,OAAO;QACL,KAAK;QACL,IAAI;QACJ,OAAO;QACP,WAAW;QACX,WAAW,EAAE,mBAAmB,CAAC,IAAI,CAAC;KACvC,CAAC;AACJ,CAAC"}
@@ -10,5 +10,11 @@ export declare class MissingBundledAssetRootError extends Error {
10
10
  readonly assetRootId: BundledAssetRootId;
11
11
  constructor(assetRootId: BundledAssetRootId);
12
12
  }
13
+ /**
14
+ * Reads `version` from the bundled package.json. Synchronous by contract: the
15
+ * MCP server info is built during (sync) server construction, and this reads a
16
+ * single small file that ships inside the package.
17
+ */
18
+ export declare function readBundledPackageVersion(packageRoot?: string): string;
13
19
  export declare function resolveBundledAssetPaths(moduleUrl?: string, runtimeAssetRoots?: readonly HarnessRuntimeAssetRoot[]): BundledAssetPaths;
14
20
  export {};
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { harnessSurfaceRegistry } from "../../harness/surface-registry.js";
@@ -26,6 +27,32 @@ function packageRootFromModulePath(modulePath) {
26
27
  }
27
28
  return path.resolve(path.dirname(modulePath), "../..");
28
29
  }
30
+ /**
31
+ * Version reported when the bundled manifest cannot be read or declares no
32
+ * string `version`. Callers embed this in server handshakes at construction
33
+ * time, so a missing/corrupt manifest must degrade rather than abort boot.
34
+ */
35
+ const UNKNOWN_PACKAGE_VERSION = "0.0.0";
36
+ /**
37
+ * Reads `version` from the bundled package.json. Synchronous by contract: the
38
+ * MCP server info is built during (sync) server construction, and this reads a
39
+ * single small file that ships inside the package.
40
+ */
41
+ export function readBundledPackageVersion(packageRoot = resolveBundledAssetPaths().packageRoot) {
42
+ let manifest;
43
+ try {
44
+ manifest = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf-8"));
45
+ }
46
+ catch (error) {
47
+ if (error instanceof Error)
48
+ return UNKNOWN_PACKAGE_VERSION;
49
+ throw error;
50
+ }
51
+ const version = typeof manifest === "object" && manifest !== null && "version" in manifest
52
+ ? manifest.version
53
+ : undefined;
54
+ return typeof version === "string" ? version : UNKNOWN_PACKAGE_VERSION;
55
+ }
29
56
  export function resolveBundledAssetPaths(moduleUrl = import.meta.url, runtimeAssetRoots = harnessSurfaceRegistry.packageAssets.runtimeAssetRoots) {
30
57
  const modulePath = fileURLToPath(moduleUrl);
31
58
  const packageRoot = packageRootFromModulePath(modulePath);
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../../src/core/runtime/assets.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,sBAAsB,EAAgC,MAAM,mCAAmC,CAAC;AAWzG,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IAChC;IAArB,YAAqB,WAA+B;QAClD,KAAK,CAAC,mDAAmD,WAAW,EAAE,CAAC,CAAC;QADrD,gBAAW,GAAX,WAAW,CAAoB;QAElD,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;IAC7C,CAAC;CACF;AAED,SAAS,wBAAwB,CAC/B,iBAAqD,EACrD,EAAsB;IAEtB,MAAM,SAAS,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7E,IAAI,SAAS,KAAK,SAAS;QAAE,MAAM,IAAI,4BAA4B,CAAC,EAAE,CAAC,CAAC;IACxE,OAAO,SAAS,CAAC,IAAI,CAAC;AACxB,CAAC;AAED,SAAS,yBAAyB,CAAC,UAAkB;IACnD,MAAM,kBAAkB,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAC9B,kBAAkB,CAAC,WAAW,CAAC,MAAM,CAAC,EACtC,kBAAkB,CAAC,WAAW,CAAC,KAAK,CAAC,CACtC,CAAC;IAEF,IAAI,eAAe,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,YAAY,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;QAClE,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC;QACzE,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,YAAoB,MAAM,CAAC,IAAI,CAAC,GAAG,EACnC,oBAAwD,sBAAsB,CAAC,aAAa,CAAC,iBAAiB;IAE9G,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,yBAAyB,CAAC,UAAU,CAAC,CAAC;IAE1D,OAAO;QACL,WAAW;QACX,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,wBAAwB,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;QAC5F,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,wBAAwB,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;QAC5F,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,wBAAwB,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;KACxG,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../../src/core/runtime/assets.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,14 @@ export interface WikilinkIndex {
8
8
  readonly byBasename: ReadonlyMap<string, readonly string[]>;
9
9
  /** Lowercase vault-relative path (with .md) → original-case vault-relative path. */
10
10
  readonly byPath: ReadonlyMap<string, string>;
11
+ /** Lowercase frontmatter alias → list of vault-relative paths declaring it. */
12
+ readonly byAlias: ReadonlyMap<string, readonly string[]>;
13
+ }
14
+ /** A vault document paired with its already-parsed frontmatter. */
15
+ export interface IndexedDoc {
16
+ /** Vault-relative path (original case, with .md). */
17
+ readonly path: string;
18
+ readonly frontmatter: Record<string, unknown>;
11
19
  }
12
20
  /** Result of resolving a single raw wikilink against the vault file set. */
13
21
  export interface WikilinkResolution {
@@ -24,6 +32,14 @@ export interface WikilinkResolution {
24
32
  * O(n) construction; call once per graph build pass.
25
33
  */
26
34
  export declare function buildWikilinkIndex(vaultFiles: readonly string[]): WikilinkIndex;
35
+ /**
36
+ * Build a wikilink index that also resolves frontmatter `aliases` declarations.
37
+ *
38
+ * Same path/basename behaviour as {@link buildWikilinkIndex}, plus a `byAlias`
39
+ * map so `[[Ataraxia]]` reaches a note whose frontmatter declares that alias.
40
+ * Non-string, blank, and non-array `aliases` values are ignored.
41
+ */
42
+ export declare function buildWikilinkIndexWithFrontmatter(docs: readonly IndexedDoc[]): WikilinkIndex;
27
43
  /**
28
44
  * Resolve a raw `[[wikilink]]` string to a vault-relative document path.
29
45
  *
@@ -31,6 +47,7 @@ export declare function buildWikilinkIndex(vaultFiles: readonly string[]): Wikil
31
47
  * 1. Exact vault-relative path (case-insensitive, `.md` optional)
32
48
  * 2. Basename match (case-insensitive) — when ambiguous, shortest path wins;
33
49
  * ties broken alphabetically
50
+ * 3. Frontmatter `aliases` match (case-insensitive), same tie-break
34
51
  *
35
52
  * Returns `{ docPath: null }` when unresolvable. Callers must **not** throw;
36
53
  * instead they should emit a `kind: "unknown-ref"` GraphEdge with weight 0.
@@ -9,6 +9,7 @@ import path from "node:path";
9
9
  export function buildWikilinkIndex(vaultFiles) {
10
10
  const byBasename = new Map();
11
11
  const byPath = new Map();
12
+ const byAlias = new Map();
12
13
  for (const f of vaultFiles) {
13
14
  // exact-path lookup (normalised to lowercase with .md)
14
15
  const normalised = f.toLowerCase();
@@ -20,7 +21,45 @@ export function buildWikilinkIndex(vaultFiles) {
20
21
  bucket.push(f);
21
22
  byBasename.set(base, bucket);
22
23
  }
23
- return { byBasename, byPath };
24
+ return { byBasename, byPath, byAlias };
25
+ }
26
+ /**
27
+ * Build a wikilink index that also resolves frontmatter `aliases` declarations.
28
+ *
29
+ * Same path/basename behaviour as {@link buildWikilinkIndex}, plus a `byAlias`
30
+ * map so `[[Ataraxia]]` reaches a note whose frontmatter declares that alias.
31
+ * Non-string, blank, and non-array `aliases` values are ignored.
32
+ */
33
+ export function buildWikilinkIndexWithFrontmatter(docs) {
34
+ const { byBasename, byPath } = buildWikilinkIndex(docs.map((d) => d.path));
35
+ const byAlias = new Map();
36
+ for (const doc of docs) {
37
+ for (const alias of aliasStrings(doc.frontmatter["aliases"])) {
38
+ const key = alias.toLowerCase();
39
+ const bucket = byAlias.get(key) ?? [];
40
+ bucket.push(doc.path);
41
+ byAlias.set(key, bucket);
42
+ }
43
+ }
44
+ return { byBasename, byPath, byAlias };
45
+ }
46
+ /** Flatten an unknown frontmatter `aliases` value to trimmed non-empty strings. */
47
+ function aliasStrings(value) {
48
+ if (Array.isArray(value))
49
+ return value.flatMap(aliasStrings);
50
+ if (typeof value !== "string")
51
+ return [];
52
+ const trimmed = value.trim();
53
+ return trimmed ? [trimmed] : [];
54
+ }
55
+ /** Shortest path wins; ties broken alphabetically. */
56
+ function bestCandidate(candidates) {
57
+ if (candidates === undefined || candidates.length === 0)
58
+ return null;
59
+ const sorted = candidates
60
+ .slice()
61
+ .sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
62
+ return sorted[0] ?? null;
24
63
  }
25
64
  // ---------------------------------------------------------------------------
26
65
  // Link parsing helpers
@@ -57,6 +96,7 @@ function cleanLinkTarget(raw) {
57
96
  * 1. Exact vault-relative path (case-insensitive, `.md` optional)
58
97
  * 2. Basename match (case-insensitive) — when ambiguous, shortest path wins;
59
98
  * ties broken alphabetically
99
+ * 3. Frontmatter `aliases` match (case-insensitive), same tie-break
60
100
  *
61
101
  * Returns `{ docPath: null }` when unresolvable. Callers must **not** throw;
62
102
  * instead they should emit a `kind: "unknown-ref"` GraphEdge with weight 0.
@@ -72,16 +112,11 @@ export function resolveWikilink(rawLink, index) {
72
112
  if (exactHit !== undefined)
73
113
  return { target, docPath: exactHit };
74
114
  // 2. Basename match — shortest path wins, ties broken alphabetically
75
- const base = path.basename(lc, ".md");
76
- const candidates = index.byBasename.get(base);
77
- if (candidates !== undefined && candidates.length > 0) {
78
- const sorted = candidates
79
- .slice()
80
- .sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
81
- const best = sorted[0] ?? null;
82
- return { target, docPath: best };
83
- }
84
- return { target, docPath: null };
115
+ const byBasenameHit = bestCandidate(index.byBasename.get(path.basename(lc, ".md")));
116
+ if (byBasenameHit !== null)
117
+ return { target, docPath: byBasenameHit };
118
+ // 3. Frontmatter alias match — same tie-break
119
+ return { target, docPath: bestCandidate(index.byAlias.get(lc)) };
85
120
  }
86
121
  // ---------------------------------------------------------------------------
87
122
  // Convenience: batch-convert wikilink strings to GraphEdge[]
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../src/engine/graph/resolver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../src/engine/graph/resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Link application: splice accepted candidates into a note body and persist
3
+ * through the capture kernel.
4
+ *
5
+ * This is the only linkify module with a side effect, and even here the effect
6
+ * is injected — `writeNote` arrives as a parameter, `node:fs` is never imported,
7
+ * and the capture kernel is referenced by TYPE only. mask.ts and suggest.ts stay
8
+ * pure; every disk decision (path safety, concept contract, postcondition
9
+ * re-read) remains the kernel's, unduplicated here.
10
+ *
11
+ * Optimistic concurrency: candidates carry absolute offsets into the body they
12
+ * were computed from. If that body changed, the offsets are lies. `applyLinks`
13
+ * therefore re-hashes the body it is handed and refuses to write on any
14
+ * mismatch, on any candidate whose recorded text no longer sits at its offsets,
15
+ * and on any overlapping pair — three independent ways the same corruption
16
+ * could otherwise reach disk.
17
+ */
18
+ import type { WriteNoteInput, WriteNoteResult } from "../../capture/safe.js";
19
+ import type { LinkCandidate } from "./types.js";
20
+ /** The capture-kernel seam apply persists through (structurally `writeNote`). */
21
+ export type WriteNoteLike = (input: WriteNoteInput) => Promise<WriteNoteResult>;
22
+ /** Everything the kernel needs to address the note, minus the body apply supplies. */
23
+ export type LinkifyWriteTarget = Omit<WriteNoteInput, "mode" | "dryRun" | "body" | "frontmatter">;
24
+ /** Why an apply attempt produced no write. */
25
+ export type ApplyRefusal = "note-changed" | "no-candidates" | "candidate-drift" | "overlapping-candidates" | "write-rejected";
26
+ /** Outcome of {@link applyLinks}: a written body, or a typed refusal. */
27
+ export type ApplyResult = {
28
+ readonly applied: true;
29
+ readonly body: string;
30
+ readonly contentHash: string;
31
+ readonly write: WriteNoteResult;
32
+ } | {
33
+ readonly applied: false;
34
+ readonly reason: ApplyRefusal;
35
+ readonly write?: WriteNoteResult;
36
+ };
37
+ /** Inputs for {@link applyLinks}, grouped so the call site stays readable. */
38
+ export interface ApplyLinksInput {
39
+ /** The note body the candidates were computed against. */
40
+ readonly body: string;
41
+ /** sha256 hex of that body, taken when the candidates were produced. */
42
+ readonly baseContentHash: string;
43
+ /** The candidates the caller (or user) accepted. */
44
+ readonly accepted: readonly LinkCandidate[];
45
+ readonly writeNote: WriteNoteLike;
46
+ readonly target: LinkifyWriteTarget;
47
+ }
48
+ /** sha256 hex digest of a note body — the optimistic-concurrency token. */
49
+ export declare function hashBody(body: string): string;
50
+ /**
51
+ * Apply accepted link candidates to a note body and persist the result.
52
+ *
53
+ * Returns `{ applied: false, reason }` — with no write attempted — whenever the
54
+ * body has drifted from the snapshot the candidates were computed against.
55
+ */
56
+ export declare function applyLinks(input: ApplyLinksInput): Promise<ApplyResult>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Link application: splice accepted candidates into a note body and persist
3
+ * through the capture kernel.
4
+ *
5
+ * This is the only linkify module with a side effect, and even here the effect
6
+ * is injected — `writeNote` arrives as a parameter, `node:fs` is never imported,
7
+ * and the capture kernel is referenced by TYPE only. mask.ts and suggest.ts stay
8
+ * pure; every disk decision (path safety, concept contract, postcondition
9
+ * re-read) remains the kernel's, unduplicated here.
10
+ *
11
+ * Optimistic concurrency: candidates carry absolute offsets into the body they
12
+ * were computed from. If that body changed, the offsets are lies. `applyLinks`
13
+ * therefore re-hashes the body it is handed and refuses to write on any
14
+ * mismatch, on any candidate whose recorded text no longer sits at its offsets,
15
+ * and on any overlapping pair — three independent ways the same corruption
16
+ * could otherwise reach disk.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ /** sha256 hex digest of a note body — the optimistic-concurrency token. */
20
+ export function hashBody(body) {
21
+ return createHash("sha256").update(body, "utf-8").digest("hex");
22
+ }
23
+ /** True when any two candidates claim overlapping body ranges. */
24
+ function hasOverlap(sorted) {
25
+ return sorted.some((candidate, index) => {
26
+ const previous = sorted[index - 1];
27
+ return previous !== undefined && candidate.startOffset < previous.endOffset;
28
+ });
29
+ }
30
+ /** True when every candidate's recorded text still sits at its offsets. */
31
+ function offsetsIntact(body, candidates) {
32
+ return candidates.every((candidate) => body.slice(candidate.startOffset, candidate.endOffset) === candidate.matchedText);
33
+ }
34
+ /** Splice replacements right-to-left so untouched offsets stay valid. */
35
+ function splice(body, sorted) {
36
+ return sorted.reduceRight((acc, candidate) => acc.slice(0, candidate.startOffset) + candidate.renderedReplacement + acc.slice(candidate.endOffset), body);
37
+ }
38
+ /**
39
+ * Apply accepted link candidates to a note body and persist the result.
40
+ *
41
+ * Returns `{ applied: false, reason }` — with no write attempted — whenever the
42
+ * body has drifted from the snapshot the candidates were computed against.
43
+ */
44
+ export async function applyLinks(input) {
45
+ if (hashBody(input.body) !== input.baseContentHash) {
46
+ return { applied: false, reason: "note-changed" };
47
+ }
48
+ if (input.accepted.length < 1) {
49
+ return { applied: false, reason: "no-candidates" };
50
+ }
51
+ const sorted = input.accepted.slice().sort((a, b) => a.startOffset - b.startOffset);
52
+ if (hasOverlap(sorted)) {
53
+ return { applied: false, reason: "overlapping-candidates" };
54
+ }
55
+ if (!offsetsIntact(input.body, sorted)) {
56
+ return { applied: false, reason: "candidate-drift" };
57
+ }
58
+ const body = splice(input.body, sorted);
59
+ const write = await input.writeNote({ ...input.target, mode: "update", dryRun: false, body });
60
+ if (write.status !== "written") {
61
+ return { applied: false, reason: "write-rejected", write };
62
+ }
63
+ return { applied: true, body, contentHash: hashBody(body), write };
64
+ }
65
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.js","sourceRoot":"","sources":["../../../src/engine/linkify/apply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Korean josa (조사) table + stem matcher for the linkify span detector.
3
+ *
4
+ * Why this exists: Korean is agglutinative, so a vault term almost never appears
5
+ * bare in prose — "아타락시아" shows up as "아타락시아를", "아타락시아는",
6
+ * "아타락시아에서". A plain word-boundary matcher misses every one of them, and a
7
+ * plain prefix matcher over-matches ("학교" inside "학교폭력").
8
+ *
9
+ * The rule this module encodes: a term matches a token when the term is a prefix
10
+ * and the remainder is a KNOWN particle from the table below (or empty). An
11
+ * unknown remainder is NOT a match — there is no morphological analysis here and
12
+ * no guessing. Latin-script terms get ordinary case-insensitive whole-token
13
+ * matching; particle logic never applies to them.
14
+ *
15
+ * Pure module: no I/O, no state. Tokenisation grain matches
16
+ * src/engine/graph/node.ts::tokenize (\p{L}/\p{N} runs), so callers can feed it
17
+ * whitespace/punctuation-delimited tokens directly.
18
+ */
19
+ /**
20
+ * Common Korean particles, ≤2 syllables, that may directly follow a noun.
21
+ *
22
+ * Plain reviewable data, grouped by role. Deliberately conservative: only
23
+ * particles that attach to a bare noun stem are listed, because anything longer
24
+ * or more contextual (verb endings such as "하게", "이라는") is exactly the case
25
+ * the matcher must reject rather than guess at.
26
+ */
27
+ export declare const KOREAN_PARTICLES: readonly ["이", "가", "께서", "은", "는", "을", "를", "의", "에", "에서", "에게", "께", "한테", "으로", "로", "와", "과", "하고", "이랑", "랑", "보다", "부터", "까지", "에도", "에는", "도", "만", "조차", "마저", "밖에", "이나", "이란", "란", "이다"];
28
+ /** A particle string drawn from {@link KOREAN_PARTICLES}. */
29
+ export type KoreanParticle = (typeof KOREAN_PARTICLES)[number];
30
+ /**
31
+ * Outcome of matching a term against a single token.
32
+ *
33
+ * Offsets are UTF-16 code-unit indices into the token, so `token.slice(stemStart,
34
+ * stemEnd)` yields the stem the caller should wrap. Particle fields are null on
35
+ * a bare match (no trailing particle).
36
+ */
37
+ export type JosaMatch = {
38
+ readonly kind: "match";
39
+ readonly stem: string;
40
+ readonly stemStart: number;
41
+ readonly stemEnd: number;
42
+ readonly particle: string | null;
43
+ readonly particleStart: number | null;
44
+ readonly particleEnd: number | null;
45
+ } | {
46
+ readonly kind: "no_match";
47
+ };
48
+ /** True when the remainder is a known particle short enough to be one. */
49
+ export declare function isKnownParticle(remainder: string): boolean;
50
+ /**
51
+ * Match `term` inside a single whitespace/punctuation-delimited `token`.
52
+ *
53
+ * Korean-final terms accept one trailing particle from {@link KOREAN_PARTICLES};
54
+ * the returned span covers the stem only. Every other trailing remainder is a
55
+ * no-match. Latin/other-script terms require the whole token to equal the term
56
+ * (case-insensitive) — no particle absorption, no partial-word hits.
57
+ */
58
+ export declare function matchTermInToken(term: string, token: string): JosaMatch;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Korean josa (조사) table + stem matcher for the linkify span detector.
3
+ *
4
+ * Why this exists: Korean is agglutinative, so a vault term almost never appears
5
+ * bare in prose — "아타락시아" shows up as "아타락시아를", "아타락시아는",
6
+ * "아타락시아에서". A plain word-boundary matcher misses every one of them, and a
7
+ * plain prefix matcher over-matches ("학교" inside "학교폭력").
8
+ *
9
+ * The rule this module encodes: a term matches a token when the term is a prefix
10
+ * and the remainder is a KNOWN particle from the table below (or empty). An
11
+ * unknown remainder is NOT a match — there is no morphological analysis here and
12
+ * no guessing. Latin-script terms get ordinary case-insensitive whole-token
13
+ * matching; particle logic never applies to them.
14
+ *
15
+ * Pure module: no I/O, no state. Tokenisation grain matches
16
+ * src/engine/graph/node.ts::tokenize (\p{L}/\p{N} runs), so callers can feed it
17
+ * whitespace/punctuation-delimited tokens directly.
18
+ */
19
+ // ---------------------------------------------------------------------------
20
+ // Particle table
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Common Korean particles, ≤2 syllables, that may directly follow a noun.
24
+ *
25
+ * Plain reviewable data, grouped by role. Deliberately conservative: only
26
+ * particles that attach to a bare noun stem are listed, because anything longer
27
+ * or more contextual (verb endings such as "하게", "이라는") is exactly the case
28
+ * the matcher must reject rather than guess at.
29
+ */
30
+ export const KOREAN_PARTICLES = [
31
+ // subject (주격)
32
+ "이",
33
+ "가",
34
+ "께서",
35
+ // topic (보조사 — 주제)
36
+ "은",
37
+ "는",
38
+ // object (목적격)
39
+ "을",
40
+ "를",
41
+ // possessive (관형격)
42
+ "의",
43
+ // locative / directional (부사격)
44
+ "에",
45
+ "에서",
46
+ "에게",
47
+ "께",
48
+ "한테",
49
+ "으로",
50
+ "로",
51
+ // comitative / conjunctive (접속)
52
+ "와",
53
+ "과",
54
+ "하고",
55
+ "이랑",
56
+ "랑",
57
+ // comparative / source (부사격)
58
+ "보다",
59
+ "부터",
60
+ "까지",
61
+ "에도",
62
+ "에는",
63
+ // auxiliary (보조사)
64
+ "도",
65
+ "만",
66
+ "조차",
67
+ "마저",
68
+ "밖에",
69
+ "이나",
70
+ "이란",
71
+ "란",
72
+ // copula attached straight to a noun
73
+ "이다",
74
+ ];
75
+ const PARTICLE_SET = new Set(KOREAN_PARTICLES);
76
+ const NO_MATCH = { kind: "no_match" };
77
+ // ---------------------------------------------------------------------------
78
+ // Matcher
79
+ // ---------------------------------------------------------------------------
80
+ const HANGUL_SYLLABLE = /^[\uAC00-\uD7A3]+$/u;
81
+ /** True when the string's last character is a Hangul syllable. */
82
+ function endsWithHangul(text) {
83
+ const last = text.slice(-1);
84
+ return last.length > 0 && HANGUL_SYLLABLE.test(last);
85
+ }
86
+ /** True when the remainder is a known particle short enough to be one. */
87
+ export function isKnownParticle(remainder) {
88
+ return remainder.length <= 2 && PARTICLE_SET.has(remainder);
89
+ }
90
+ /**
91
+ * Match `term` inside a single whitespace/punctuation-delimited `token`.
92
+ *
93
+ * Korean-final terms accept one trailing particle from {@link KOREAN_PARTICLES};
94
+ * the returned span covers the stem only. Every other trailing remainder is a
95
+ * no-match. Latin/other-script terms require the whole token to equal the term
96
+ * (case-insensitive) — no particle absorption, no partial-word hits.
97
+ */
98
+ export function matchTermInToken(term, token) {
99
+ if (term.length < 1 || token.length < 1)
100
+ return NO_MATCH;
101
+ if (!endsWithHangul(term)) {
102
+ return token.toLowerCase() === term.toLowerCase() ? bareMatch(term.length, token) : NO_MATCH;
103
+ }
104
+ if (!token.startsWith(term))
105
+ return NO_MATCH;
106
+ const remainder = token.slice(term.length);
107
+ if (remainder.length < 1)
108
+ return bareMatch(term.length, token);
109
+ if (!isKnownParticle(remainder))
110
+ return NO_MATCH;
111
+ return {
112
+ kind: "match",
113
+ stem: token.slice(0, term.length),
114
+ stemStart: 0,
115
+ stemEnd: term.length,
116
+ particle: remainder,
117
+ particleStart: term.length,
118
+ particleEnd: token.length,
119
+ };
120
+ }
121
+ /** Whole-token match with no trailing particle. */
122
+ function bareMatch(stemEnd, token) {
123
+ return {
124
+ kind: "match",
125
+ stem: token.slice(0, stemEnd),
126
+ stemStart: 0,
127
+ stemEnd,
128
+ particle: null,
129
+ particleStart: null,
130
+ particleEnd: null,
131
+ };
132
+ }
133
+ //# sourceMappingURL=josa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"josa.js","sourceRoot":"","sources":["../../../src/engine/linkify/josa.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,eAAe;IACf,GAAG;IACH,GAAG;IACH,IAAI;IACJ,mBAAmB;IACnB,GAAG;IACH,GAAG;IACH,eAAe;IACf,GAAG;IACH,GAAG;IACH,mBAAmB;IACnB,GAAG;IACH,+BAA+B;IAC/B,GAAG;IACH,IAAI;IACJ,IAAI;IACJ,GAAG;IACH,IAAI;IACJ,IAAI;IACJ,GAAG;IACH,gCAAgC;IAChC,GAAG;IACH,GAAG;IACH,IAAI;IACJ,IAAI;IACJ,GAAG;IACH,6BAA6B;IAC7B,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,kBAAkB;IAClB,GAAG;IACH,GAAG;IACH,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,GAAG;IACH,qCAAqC;IACrC,IAAI;CACI,CAAC;AAKX,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAS,gBAAgB,CAAC,CAAC;AAyB5E,MAAM,QAAQ,GAAc,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAEjD,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,MAAM,eAAe,GAAG,qBAAqB,CAAC;AAE9C,kEAAkE;AAClE,SAAS,cAAc,CAAC,IAAY;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvD,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,OAAO,SAAS,CAAC,MAAM,IAAI,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IAEzD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC/F,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE7C,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC/D,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC;QAAE,OAAO,QAAQ,CAAC;IAEjD,OAAO;QACL,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC;QACjC,SAAS,EAAE,CAAC;QACZ,OAAO,EAAE,IAAI,CAAC,MAAM;QACpB,QAAQ,EAAE,SAAS;QACnB,aAAa,EAAE,IAAI,CAAC,MAAM;QAC1B,WAAW,EAAE,KAAK,CAAC,MAAM;KAC1B,CAAC;AACJ,CAAC;AAED,mDAAmD;AACnD,SAAS,SAAS,CAAC,OAAe,EAAE,KAAa;IAC/C,OAAO;QACL,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC;QAC7B,SAAS,EAAE,CAAC;QACZ,OAAO;QACP,QAAQ,EAAE,IAAI;QACd,aAAa,EAAE,IAAI;QACnB,WAAW,EAAE,IAAI;KAClB,CAAC;AACJ,CAAC"}