dsh-prime-memory 0.22.1 → 0.24.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 (121) hide show
  1. package/CHANGELOG.en.md +401 -0
  2. package/CHANGELOG.ja.md +401 -0
  3. package/CHANGELOG.ko.md +401 -0
  4. package/CHANGELOG.md +100 -0
  5. package/dist/client.js +1951 -1188
  6. package/dist/config.d.ts +176 -1240
  7. package/dist/config.js +113 -2
  8. package/dist/conflict-service.d.ts +12 -0
  9. package/dist/conflict-service.js +24 -0
  10. package/dist/contract.d.ts +327 -0
  11. package/dist/domain-gate.d.ts +19 -0
  12. package/dist/domain-gate.js +45 -0
  13. package/dist/graph/constraints.d.ts +4 -2
  14. package/dist/graph/constraints.js +4 -2
  15. package/dist/hooks/obsolete-notice.d.ts +26 -0
  16. package/dist/hooks/obsolete-notice.js +64 -0
  17. package/dist/hooks/recall.d.ts +41 -0
  18. package/dist/hooks/recall.js +127 -28
  19. package/dist/hooks/skill-recall.d.ts +25 -0
  20. package/dist/hooks/skill-recall.js +72 -0
  21. package/dist/index.d.ts +1 -1029
  22. package/dist/index.js +55 -2
  23. package/dist/memory-types.d.ts +69 -0
  24. package/dist/memory-types.js +165 -0
  25. package/dist/memport/format.d.ts +75 -0
  26. package/dist/memport/format.js +345 -0
  27. package/dist/memport/prefilter.d.ts +62 -0
  28. package/dist/memport/prefilter.js +132 -0
  29. package/dist/memport/scan.d.ts +13 -0
  30. package/dist/memport/scan.js +100 -0
  31. package/dist/metadata-validators.d.ts +22 -0
  32. package/dist/metadata-validators.js +45 -0
  33. package/dist/pipeline/l1.d.ts +44 -2
  34. package/dist/pipeline/l1.js +177 -9
  35. package/dist/pipeline/md-sync.d.ts +65 -0
  36. package/dist/pipeline/md-sync.js +456 -0
  37. package/dist/pipeline/ruminate.d.ts +46 -1
  38. package/dist/pipeline/ruminate.js +150 -1
  39. package/dist/pipeline/runner.d.ts +10 -0
  40. package/dist/pipeline/runner.js +7 -1
  41. package/dist/pipeline/slot-migrate.d.ts +45 -0
  42. package/dist/pipeline/slot-migrate.js +45 -0
  43. package/dist/pipeline/workspace-relink.d.ts +60 -0
  44. package/dist/pipeline/workspace-relink.js +73 -0
  45. package/dist/projection/recall.d.ts +112 -0
  46. package/dist/projection/recall.js +183 -0
  47. package/dist/projection/slots.d.ts +5 -0
  48. package/dist/prompts/examples.d.ts +73 -0
  49. package/dist/prompts/examples.js +178 -0
  50. package/dist/prompts/graph-projection.js +3 -0
  51. package/dist/prompts/l1-dedup.d.ts +9 -3
  52. package/dist/prompts/l1-dedup.js +34 -13
  53. package/dist/prompts/l1-extraction.d.ts +9 -4
  54. package/dist/prompts/l1-extraction.js +76 -8
  55. package/dist/settings.d.ts +8 -0
  56. package/dist/settings.js +15 -1
  57. package/dist/skills/crystallize.d.ts +41 -0
  58. package/dist/skills/crystallize.js +75 -0
  59. package/dist/stats.d.ts +30 -1
  60. package/dist/stats.js +504 -13
  61. package/dist/store/embedding-source.d.ts +8 -0
  62. package/dist/store/embedding-source.js +39 -2
  63. package/dist/store/file-versions.d.ts +4 -0
  64. package/dist/store/file-versions.js +4 -0
  65. package/dist/store/graph-store.d.ts +14 -0
  66. package/dist/store/graph-store.js +35 -0
  67. package/dist/store/l1-snapshot.d.ts +26 -1
  68. package/dist/store/l1-snapshot.js +53 -8
  69. package/dist/store/l1.d.ts +77 -3
  70. package/dist/store/l1.js +128 -3
  71. package/dist/store/memory-md-archive.d.ts +84 -0
  72. package/dist/store/memory-md-archive.js +155 -0
  73. package/dist/store/memory-md-disable.d.ts +23 -0
  74. package/dist/store/memory-md-disable.js +106 -0
  75. package/dist/store/memory-md-export.d.ts +50 -0
  76. package/dist/store/memory-md-export.js +200 -0
  77. package/dist/store/memory-md-ledger.d.ts +94 -0
  78. package/dist/store/memory-md-ledger.js +243 -0
  79. package/dist/store/memory-md-orphan.d.ts +27 -0
  80. package/dist/store/memory-md-orphan.js +153 -0
  81. package/dist/store/memory-md-parse.d.ts +35 -0
  82. package/dist/store/memory-md-parse.js +85 -0
  83. package/dist/store/memory-md-skeleton.d.ts +18 -0
  84. package/dist/store/memory-md-skeleton.js +61 -0
  85. package/dist/store/memory-md-types.d.ts +174 -0
  86. package/dist/store/memory-md-types.js +10 -0
  87. package/dist/store/memory-md.d.ts +108 -0
  88. package/dist/store/memory-md.js +219 -0
  89. package/dist/store/memory-sources.d.ts +59 -0
  90. package/dist/store/memory-sources.js +214 -0
  91. package/dist/store/obsolete-notices.d.ts +39 -0
  92. package/dist/store/obsolete-notices.js +153 -0
  93. package/dist/store/recall-feedback.d.ts +75 -0
  94. package/dist/store/recall-feedback.js +213 -0
  95. package/dist/store/recall-usage.d.ts +11 -0
  96. package/dist/store/recall-usage.js +29 -0
  97. package/dist/store/session-recalled.d.ts +60 -0
  98. package/dist/store/session-recalled.js +310 -0
  99. package/dist/store/skill-store.d.ts +99 -0
  100. package/dist/store/skill-store.js +330 -0
  101. package/dist/store/sqlite.d.ts +71 -0
  102. package/dist/store/sqlite.js +259 -15
  103. package/dist/store/supersede.d.ts +1 -1
  104. package/dist/store/supersede.js +1 -1
  105. package/dist/tools/index.d.ts +4 -13
  106. package/dist/tools/index.js +80 -12
  107. package/dist/tools/skills.d.ts +13 -0
  108. package/dist/tools/skills.js +130 -0
  109. package/dist/types.d.ts +30 -7
  110. package/dist/types.js +10 -2
  111. package/dist/util/maturity.d.ts +44 -0
  112. package/dist/util/maturity.js +43 -0
  113. package/dist/util/md-name.d.ts +129 -0
  114. package/dist/util/md-name.js +366 -0
  115. package/dist/util/md-normalize.d.ts +23 -0
  116. package/dist/util/md-normalize.js +131 -0
  117. package/dist/wing-backfill.js +7 -4
  118. package/dist/workspace-route.d.ts +22 -0
  119. package/dist/workspace-route.js +66 -0
  120. package/dsh.plugin.json +1 -1
  121. package/package.json +6 -5
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 技能工具面(轨2)。**整块仅在 cfg.skills.enabled 时注册**——默认关时零表面积
3
+ * (对齐 benchControl"生产部署零表面积"先例,避免注册了却被门控的空壳工具污染模型工具列表)。
4
+ *
5
+ * 读工具 list/read;写工具 pin/archive 过 live.memoryMutate 高权限门(与 memory_add 同门)。
6
+ * 技能不是 memory `type`,这些工具只操作 SkillStore sidecar,不触碰 l1_records / ADD_TYPES / prompt 词表。
7
+ */
8
+ import type { Context } from '@deepseek-ai/cordis';
9
+ import type { MemoryConfig } from '../config.js';
10
+ import type { LiveSettingsHandle } from '../settings.js';
11
+ import type { SkillStore } from '../store/skill-store.js';
12
+ import type { MemoryLogger } from '../types.js';
13
+ export declare function registerSkillTools(ctx: Context, cfg: MemoryConfig, skills: SkillStore, logger: MemoryLogger, live: LiveSettingsHandle): void;
@@ -0,0 +1,130 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ const MUTATE_OFF_NOTICE = '记忆写删未开放:请在记忆库面板开启「高权限模式」后,模型才能改动技能。';
3
+ export function registerSkillTools(ctx, cfg, skills, logger, live) {
4
+ if (!(cfg.skills.enabled === true || live.get().skillsEnabled === true))
5
+ return; // 默认关:部署 cfg 或看板 live 任一开才注册
6
+ // memory_skill_list:列全部技能摘要(含 status/sessionCount,candidate 也列出供审阅)。
7
+ ctx.tools.register(defineTool({
8
+ name: 'memory_skill_list',
9
+ description: '列出已沉淀的技能(程序性记忆)摘要:id/标题/状态/档位/覆盖会话数。用于查看技能库现状与候选晋升情况。',
10
+ parameters: {},
11
+ output: {
12
+ schema: {
13
+ type: 'object',
14
+ properties: {
15
+ skills: {
16
+ type: 'array',
17
+ items: {
18
+ type: 'object',
19
+ properties: {
20
+ id: { type: 'string' },
21
+ title: { type: 'string' },
22
+ status: { type: 'string' },
23
+ tier: { type: 'string' },
24
+ pinned: { type: 'boolean' },
25
+ sessionCount: { type: 'number' },
26
+ },
27
+ additionalProperties: false,
28
+ },
29
+ },
30
+ notice: { type: 'string' },
31
+ },
32
+ additionalProperties: false,
33
+ },
34
+ render: (_a, v) => [{ type: 'text', text: v.notice ?? `共 ${v.skills?.length ?? 0} 个技能` }],
35
+ },
36
+ execute: async () => {
37
+ const list = skills.list().map((s) => ({
38
+ id: s.id,
39
+ title: s.title,
40
+ status: s.status,
41
+ tier: s.injectionTier,
42
+ pinned: s.pinned,
43
+ sessionCount: skills.sessionCount(s.id),
44
+ }));
45
+ return { skills: list, notice: `共 ${list.length} 个技能` };
46
+ },
47
+ }));
48
+ // memory_skill_read:取某技能完整正文 + 步骤。
49
+ ctx.tools.register(defineTool({
50
+ name: 'memory_skill_read',
51
+ description: '读取一条技能的完整正文与步骤清单(按 id)。用于展开注入里被截断/excerpt 档的技能细节。',
52
+ parameters: { id: { type: 'string', required: true, description: '技能 id(来自 memory_skill_list 或注入头)' } },
53
+ output: {
54
+ schema: {
55
+ type: 'object',
56
+ properties: {
57
+ id: { type: 'string' },
58
+ title: { type: 'string' },
59
+ body: { type: 'string' },
60
+ steps: { type: 'array', items: { type: 'string' } },
61
+ notice: { type: 'string' },
62
+ },
63
+ additionalProperties: false,
64
+ },
65
+ render: (_a, v) => [
66
+ {
67
+ type: 'text',
68
+ text: v.notice ?? [`【${v.title}】`, v.body, ...(v.steps || []).map((s) => ` - ${s}`)].join('\n'),
69
+ },
70
+ ],
71
+ },
72
+ execute: async (args) => {
73
+ const id = String(args?.id ?? '').trim();
74
+ const s = skills.get(id);
75
+ if (!s)
76
+ return { id, title: '', body: '', steps: [], notice: `未找到技能 ${id}` };
77
+ return { id: s.id, title: s.title, body: s.body, steps: s.steps };
78
+ },
79
+ }));
80
+ // memory_skill_pin:pin/unpin(高权限门)。pinned 技能不受闲置归档影响。
81
+ ctx.tools.register(defineTool({
82
+ name: 'memory_skill_pin',
83
+ description: '固定/取消固定一条技能(按 id)。固定后不参与闲置自动归档。仅用户显式要求时用;需高权限模式。',
84
+ parameters: {
85
+ id: { type: 'string', required: true, description: '技能 id' },
86
+ pinned: { type: 'boolean', required: true, description: 'true=固定,false=取消固定' },
87
+ },
88
+ output: {
89
+ schema: {
90
+ type: 'object',
91
+ properties: { notice: { type: 'string' } },
92
+ additionalProperties: false,
93
+ },
94
+ render: (_a, v) => [{ type: 'text', text: v.notice ?? '' }],
95
+ },
96
+ execute: async (args) => {
97
+ if (!live.get().memoryMutate)
98
+ return { notice: MUTATE_OFF_NOTICE };
99
+ const id = String(args?.id ?? '').trim();
100
+ const changed = await skills.update(id, { pinned: args?.pinned === true });
101
+ return { notice: changed ? `已${args?.pinned === true ? '固定' : '取消固定'}技能 ${id}` : `未找到技能 ${id} 或无变化` };
102
+ },
103
+ }));
104
+ // memory_skill_archive:归档(高权限门);走 sidecar 生命周期,不触碰 l1_records 退场。
105
+ ctx.tools.register(defineTool({
106
+ name: 'memory_skill_archive',
107
+ description: '归档一条技能(按 id,可给原因)。归档后不再注入,但记录保留可恢复。仅用户显式要求时用;需高权限模式。',
108
+ parameters: {
109
+ id: { type: 'string', required: true, description: '技能 id' },
110
+ reason: { type: 'string', description: '归档原因(可选,供审计)' },
111
+ },
112
+ output: {
113
+ schema: {
114
+ type: 'object',
115
+ properties: { notice: { type: 'string' } },
116
+ additionalProperties: false,
117
+ },
118
+ render: (_a, v) => [{ type: 'text', text: v.notice ?? '' }],
119
+ },
120
+ execute: async (args) => {
121
+ if (!live.get().memoryMutate)
122
+ return { notice: MUTATE_OFF_NOTICE };
123
+ const id = String(args?.id ?? '').trim();
124
+ const done = await skills.archive(id, String(args?.reason ?? 'manual').slice(0, 120) || 'manual');
125
+ if (done)
126
+ logger.info(`[memory] 技能归档 ${id}`);
127
+ return { notice: done ? `已归档技能 ${id}` : `未找到技能 ${id} 或已归档` };
128
+ },
129
+ }));
130
+ }
package/dist/types.d.ts CHANGED
@@ -1,9 +1,3 @@
1
- /**
2
- * 领域类型与词汇表(净室重写)。
3
- *
4
- * 覆盖:记忆族/档位、Hall 目录、L0/L1 记录形状、抽取产出与族判定三级兜底、
5
- * L2 场景摘要与 L1 检索命中。字段名与取值是磁盘/管线两侧的既定契约,不可更名。
6
- */
7
1
  /** 蒸馏 Prompt 家族:chat = 个人记忆(persona/episodic/instruction + 用户画像),work = 工作记忆(work_fact/work_task/work_method/work_artifact + Team Operating Doctrine)。 */
8
2
  export type MemoryFamily = 'chat' | 'work';
9
3
  /** 会话记忆档位:auto = 双族自动判定 | chat/work = 单族 | off = 本会话对记忆系统隐身。 */
@@ -46,7 +40,14 @@ export interface RoomCount {
46
40
  /** 人类可读名(注册表条目才有;面板 chip 可用 label 展示)。 */
47
41
  label?: string;
48
42
  }
49
- /** 记录族标签推断:work_* 前缀 → work,其余(含 auto 档兜底)→ chat。 */
43
+ /**
44
+ * 记录族标签推断(pm-name-distill task_1.2):查 `TYPE_FAMILY_MAP`(词表单一真源)。
45
+ *
46
+ * 旧实现是 `type.startsWith('work')` 前缀猜族——新词表里 `project_*`/`development_*`/
47
+ * `*_experience` 都是 work 族却不带 work 前缀,前缀判定会把它们静默归进 chat,
48
+ * 而 family 决定召回/浏览/去重的共享面。未知值仍回落旧前缀规则并 warn 一次(可观测,
49
+ * 不静默)。
50
+ */
50
51
  export declare function familyForType(type: string): MemoryFamily;
51
52
  /**
52
53
  * 记忆的持续性(时间轴第三维,与 createdAt/updatedAt 正交):
@@ -118,6 +119,13 @@ export interface ExtractedMemory {
118
119
  metadata: Record<string, unknown>;
119
120
  /** 所属情境名(L1 抽取的情境切分结果)。 */
120
121
  scene_name: string;
122
+ /**
123
+ * 名称蒸馏(Phase 3):模型**当场**给的短标题(名词短语,回答“这条讲的是什么”),
124
+ * 与 `content`(完整叙述正文)分离。只在 `cfg.naming.enabled` 时被要求产出;
125
+ * 缺省或过不了 `validateTitle` 形状闸就**不写成名字**,仍由 `L1Store.assignMissingNames`
126
+ * 兜底——名字的唯一真源不因多一个来源就分两套判据。
127
+ */
128
+ title?: string;
121
129
  /** auto 档抽取输出的显式族判定(chat|work;纯档 Prompt 无此字段)。
122
130
  * 语境归族、形状不归族——避免"个人计划性事实被 work_* 形状吸走"的族错标。 */
123
131
  family?: string;
@@ -226,6 +234,21 @@ export interface MemoryRecord {
226
234
  repoKeyOwner?: string;
227
235
  /** 记录级 applicability(ADR-0015 T1.10):'' 未声明(不围栏)| 'this-repo' | 'cross-project';显式不从 family 推导。 */
228
236
  applicability?: string;
237
+ /**
238
+ * 外部导入幂等键(轨3-P0):`sha1(source_id \0 身份)`;非外部导入(内部蒸馏/add)= undefined/''。
239
+ * 非空值受 `l1_records` 上部分唯一索引约束——同一 (来源, 内容/稳定身份) 重复导入结构性不可能。
240
+ * 与 `scene_name`/`metadata.origin` 不同:这是**存储层强制去重键**,不是展示用的溯源标签。
241
+ */
242
+ extKey?: string;
243
+ /**
244
+ * 文件身份键(md 投射相,N1):人可读、可编辑、全库唯一,直接当
245
+ * `<root>/<type>/<name>.memory.md` 的文件名。与 `extKey` 的分工:extKey 是
246
+ * **导入幂等键**(机器算的、来源内唯一),name 是**文件身份**(人写的、全库唯一)。
247
+ * 不用 id 当身份:id 每轮新铸、从不复用;不用 scene_name:它的基数大于记录数。
248
+ * undefined = 还没名字(存量记录),投射器则回落 `mem-<id8>`。
249
+ * 非空值受 `l1_records` 上的部分唯一索引约束;该列**不进**快照哈希投影。
250
+ */
251
+ name?: string;
229
252
  }
230
253
  /** L2 场景块摘要(META 解析结果)。 */
231
254
  export interface SceneSummary {
package/dist/types.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * 覆盖:记忆族/档位、Hall 目录、L0/L1 记录形状、抽取产出与族判定三级兜底、
5
5
  * L2 场景摘要与 L1 检索命中。字段名与取值是磁盘/管线两侧的既定契约,不可更名。
6
6
  */
7
+ import { familyOf } from './memory-types.js';
7
8
  export const WING_CATALOG = [
8
9
  { id: 'work', label: '工作' },
9
10
  { id: 'relationships', label: '人际' },
@@ -27,9 +28,16 @@ export function wingLabel(id) {
27
28
  const h = WING_CATALOG.find((x) => x.id === id);
28
29
  return h ? h.label : id;
29
30
  }
30
- /** 记录族标签推断:work_* 前缀 → work,其余(含 auto 档兜底)→ chat。 */
31
+ /**
32
+ * 记录族标签推断(pm-name-distill task_1.2):查 `TYPE_FAMILY_MAP`(词表单一真源)。
33
+ *
34
+ * 旧实现是 `type.startsWith('work')` 前缀猜族——新词表里 `project_*`/`development_*`/
35
+ * `*_experience` 都是 work 族却不带 work 前缀,前缀判定会把它们静默归进 chat,
36
+ * 而 family 决定召回/浏览/去重的共享面。未知值仍回落旧前缀规则并 warn 一次(可观测,
37
+ * 不静默)。
38
+ */
31
39
  export function familyForType(type) {
32
- return type.startsWith('work') ? 'work' : 'chat';
40
+ return familyOf(type);
33
41
  }
34
42
  export const PERSISTENCE_VALUES = ['p', 's', 'o', 't'];
35
43
  /** 持续性取值归一:只认 p/s/o/t,其余(缺省/非法)返回 undefined。 */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 成熟度(maturity)合成——G3,单一事实源的纯函数。
3
+ *
4
+ * 把四个**已存在**的信号合成一个可解释的置信档,供面板显示与召回排序:
5
+ * - `priority`(L1 真实列,0-100):写入时的重要性先验;
6
+ * - `usage.used`(recall-usage,进 topN 被注入才计):被反复用到 = 越"确立";
7
+ * - `agingWeight`(读路径衰减系数 0.5~1,null=关):新鲜度;
8
+ * - `mark`(人在召回卡上打的 -1..5,**只看最近一笔**):显式的人工裁决。
9
+ *
10
+ * 人工标记是**加性**项(±W_MARK),不是权重再分配:未打分时贡献恒为 0,
11
+ * 与三输入时代的分值**逐字相等**(旧回归断言不会因新增一项而漂)。
12
+ * 档位设计(用户口径:0/1/2 拉低、4/5 抬高):0/1/2 为负、3 中性、4/5 为正;
13
+ * `-1`(声明过时)**不在此计分**——它走退场,若再计一次罚就是同一件事罚两遍。
14
+ *
15
+ * 刻意**读时派生、不落库**(Agent B/C 结论):落库会在每轮 recordAttempt 触发双写 + 值随时间陈旧,
16
+ * 且顶层列不进快照哈希=隐形改写。各输入即时可得,纯函数零 IO。
17
+ *
18
+ * 权重/阈值集中为常量,便于回归与调参;缺 usage(未装配)按 0 强化处理(不虚构成熟度)。
19
+ */
20
+ export type Maturity = 'low' | 'mid' | 'high';
21
+ export interface MaturityUsage {
22
+ used: number;
23
+ }
24
+ export interface MaturityInput {
25
+ priority: number;
26
+ usage?: MaturityUsage | null;
27
+ /** 读路径老化权重(0.5~1);null = 衰减关闭 → 视为 1(不惩罚)。 */
28
+ agingWeight?: number | null;
29
+ /**
30
+ * 人给的最近一笔打分(-1..5);未打分 = undefined/null → 贡献 0(逐字旧行为)。
31
+ * `-1` 也按 0 处理:那条记录已经退场,不该在成熟度上再罚一次。
32
+ */
33
+ mark?: number | null;
34
+ }
35
+ /**
36
+ * 打分 → 归一化人工信号(-1..1)。3 分与未打分同为中性。
37
+ * 正侧只分两档(4 半、5 满);负侧分三档渐次加重(2 轻罚 -0.4、1 中罚 -0.7、0 重罚 -1)——
38
+ * “不相关”是个需要区分程度的否定,而“相关”不必分那么细。
39
+ */
40
+ export declare function markSignal(mark: number | null | undefined): number;
41
+ /** 合成分值(0~1),导出供测试与未来排序复用。 */
42
+ export declare function maturityScore(i: MaturityInput): number;
43
+ /** 三档成熟度:high ≥0.66、mid ≥0.40、否则 low。 */
44
+ export declare function computeMaturity(i: MaturityInput): Maturity;
@@ -0,0 +1,43 @@
1
+ const W_PRIORITY = 0.45;
2
+ const W_USAGE = 0.3;
3
+ const W_AGING = 0.25;
4
+ /** 人工标记的加性幅度(±0.3):够让一档在强先验上被推翻,又不足以一票定生死。 */
5
+ const W_MARK = 0.3;
6
+ /** used 达到该数即满强化(再高不加分)。 */
7
+ const USAGE_SATURATE = 5;
8
+ const HIGH_THRESHOLD = 0.66;
9
+ const MID_THRESHOLD = 0.4;
10
+ /**
11
+ * 打分 → 归一化人工信号(-1..1)。3 分与未打分同为中性。
12
+ * 正侧只分两档(4 半、5 满);负侧分三档渐次加重(2 轻罚 -0.4、1 中罚 -0.7、0 重罚 -1)——
13
+ * “不相关”是个需要区分程度的否定,而“相关”不必分那么细。
14
+ */
15
+ export function markSignal(mark) {
16
+ switch (mark) {
17
+ case 5: return 1;
18
+ case 4: return 0.5;
19
+ case 2: return -0.4;
20
+ case 1: return -0.7;
21
+ case 0: return -1;
22
+ default: return 0; // 3 / -1 / 未打分 / 越界值
23
+ }
24
+ }
25
+ function clamp01(x) {
26
+ return Math.max(0, Math.min(1, x));
27
+ }
28
+ /** 合成分值(0~1),导出供测试与未来排序复用。 */
29
+ export function maturityScore(i) {
30
+ const p = clamp01(i.priority / 100);
31
+ const u = i.usage ? clamp01(i.usage.used / USAGE_SATURATE) : 0;
32
+ const a = i.agingWeight == null ? 1 : clamp01(i.agingWeight);
33
+ return clamp01(W_PRIORITY * p + W_USAGE * u + W_AGING * a + W_MARK * markSignal(i.mark));
34
+ }
35
+ /** 三档成熟度:high ≥0.66、mid ≥0.40、否则 low。 */
36
+ export function computeMaturity(i) {
37
+ const s = maturityScore(i);
38
+ if (s >= HIGH_THRESHOLD)
39
+ return 'high';
40
+ if (s >= MID_THRESHOLD)
41
+ return 'mid';
42
+ return 'low';
43
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 入库闸门的长度上限(字符)。人/模型**有意给**的名字允许到这里。
3
+ *
4
+ * 历史:24 → 28 → **40**。放宽的理由不是“名字长点好看”,是**计数把 ASCII 与汉字同价**:
5
+ * 本项目记忆标题里 `dsh-agent-teams`、`Python-3.13`、`NeatDownloader` 这类标识符很常见,
6
+ * 一个词就吃掉 13–16 个额度,于是语义上只有一两个词的正当名在字符上“超长”
7
+ * (真机 13 条 fallback 里 3 条属此类)。28 的代价不是难看,是把已经拿到的语义丢掉重算。
8
+ * 仍钉在 40 而不是更松:投射路径要留在 Windows `MAX_PATH` 里,而且超过 40 的名
9
+ * 几乎必然是句子而不是标题。
10
+ */
11
+ export declare const NAME_MAX = 40;
12
+ /**
13
+ * 规则兜底名({@link deriveName})的截断上限,**故意比闸门紧**。
14
+ *
15
+ * 两者不能合为一个数(实测过:统一放 40 后,`buildTurnCompactSystemPrompt` 会拖出
16
+ * 「负责组装每一轮的系统提示」这种句子尾巴):`deriveName` 吃的是**叙述句**,多给的额度
17
+ * 只会被句子吃掉;而闸门吃的是**人/模型写的标题**,那里长度是语义而不是噪声。
18
+ * 28 仍是兜底名不至于变成句子的上限。
19
+ */
20
+ export declare const NAME_CLIP_MAX = 28;
21
+ /** 目标区间下界(推荐名短于此偏空),仅作注释性常量,不作硬闸。 */
22
+ export declare const NAME_TARGET_MIN = 6;
23
+ /** 目标区间上界(推荐名长于此偏啰嗦),仅作注释性常量,不作硬闸。 */
24
+ export declare const NAME_TARGET_MAX = 14;
25
+ /**
26
+ * 叙述引子表:正文是"谁干了什么"的完整叙述句时,这些前缀只是话头,
27
+ * 占了名字名额却不含信息量(真机坏味「用户要求将-...」「团队已完成-...」即此)。
28
+ * 导出供 `validateTitle` 与 `deriveName` **共用同一份**,两处判据不许漂移。
29
+ */
30
+ export declare const NARRATIVE_LEAD_INS: readonly string[];
31
+ /**
32
+ * 正文 → 名字干(分段流水线)。
33
+ *
34
+ * 保持签名 `(content) => string` 与"洗不出可用文字返回空串"的承诺。步骤:
35
+ * 1. 标题行优先:正文有 `^#{1,3} ` 行用它,否则取首个非空行;
36
+ * 2. 剥 markdown 前缀;
37
+ * 3. 剥叙述引子({@link NARRATIVE_LEAD_INS})连同其后标点/残留话头;
38
+ * 4. 结构边界截断(第一个句读 / 括号 / 反引号 / 路径 / session / uuid 处断开,
39
+ * 括号内整块丢弃——除非主句因此被丢空);
40
+ * 5. 词元边界收尾:CJK 逐字、ASCII 逐词,绝不切半单词;
41
+ * 6. 截断纪律:过 `sanitizeFilename`,剥尾随 `.` `-` 空格。
42
+ *
43
+ * @param content - 记忆正文。
44
+ * @returns 文件名安全的名字干;无可用文字时 `''`。
45
+ */
46
+ export declare function deriveName(content: string): string;
47
+ /**
48
+ * 步骤 5:截到 maxLen,且绝不切半 ASCII 单词。
49
+ * 若刚好落在 ASCII 词元中间(末字符与被丢字符都是 `[A-Za-z0-9]`),回删整个残词。
50
+ * CJK 逐字,无需回退。
51
+ *
52
+ * 导出是因为**改名侧也要同一个收梢算法**:模型给的 title 只是超长(`too-long`)时,
53
+ * 按词元边界收一刀仍是好名字,直接回落 `deriveName` 等于把已经拿到的语义扔掉重算。
54
+ * 两处共用一份,截断形状才不会分头演化。
55
+ */
56
+ export declare function clipAtTokenBoundary(raw: string, maxLen: number): string;
57
+ /**
58
+ * 校验一个**人给的或模型给的**名字是否可用(形状,不含唯一性)。
59
+ *
60
+ * Phase 3 的 few-shot 判据会拿它当门禁,因此签名与 reason 取值必须稳定。
61
+ * 命中任一硬规则即拒;全部通过返回 `{ ok: true, name }`(name 为过字符白名单后的归一值)。
62
+ *
63
+ * @param raw - 候选名字。
64
+ * @returns `{ ok: true, name }` 或 `{ ok: false, reason }`。
65
+ */
66
+ export declare function validateTitle(raw: string): {
67
+ ok: true;
68
+ name: string;
69
+ } | {
70
+ ok: false;
71
+ reason: string;
72
+ };
73
+ /**
74
+ * 剥一个**模型给的** title 的形状噪声:空白(含全角空格)→ `-`、连字折叠、首尾点/横线剔掉。
75
+ *
76
+ * 为什么需要它:不是语义不对,只是字符形状不对——直接拿原文过形状闸会把
77
+ * “尾部多一个点”这种一律打进 fallback(真机旧行为:16/20 条白归规则名)。
78
+ * 导出是为了**只有一个整形函数**:管线与临时改名脚本共用,两套迟早漂。
79
+ */
80
+ export declare function reshapeTitle(raw: string): string;
81
+ /**
82
+ * 收下模型产出的 title:先整形再入闸,**只超长**时按词元边界再收一刀。
83
+ *
84
+ * 返回落名或 `undefined`(不合格)。刻意**不回落规则名**:那是调用方的决定,
85
+ * 因为两条路的可观测要求不同(管线要“没名字就交给起名器”,脚本要“记 fallback+reason”)。
86
+ * 闸仍是 {@link validateTitle} 那一个,不开第二套判据。
87
+ *
88
+ * @param raw - 模型给的原文(故意收 `unknown`:模型输出是不可信输入)。
89
+ * @returns 可用的落名,或 `undefined`。
90
+ */
91
+ export declare function acceptTitle(raw: unknown): string | undefined;
92
+ /**
93
+ * 校验一个**人给的**名字是否可用(形状,不含唯一性)。
94
+ *
95
+ * 人从面板或 md 文件里改名字时,先过这道形状闸再谈唯一性:形状不合就拒,
96
+ * 比"收下然后写不出文件"好。返回归一后的名字(可能与入参不同:洗掉了非法字符)。
97
+ *
98
+ * @param raw - 用户给的名字。
99
+ * @returns `{ ok: true, name }` 或 `{ ok: false, reason }`。
100
+ */
101
+ export declare function normalizeName(raw: string): {
102
+ ok: true;
103
+ name: string;
104
+ } | {
105
+ ok: false;
106
+ reason: string;
107
+ };
108
+ /** 名字没洗出可用字符时的回落干:`mem-<id 后 8 位>`。 */
109
+ export declare function fallbackStem(id: string): string;
110
+ /**
111
+ * 给一个名字求**全库唯一**的落名:基底 → `基底-2` → `基底-3` → … → 回落名。
112
+ *
113
+ * 为什么必须在这里消歧,而不是"谁先到谁写":同一段话的多条记录必然洗出同一个名字。
114
+ * 真机实测 1166 条里 42 条挤进 17 个同名文件(最狠一个路径 6 条),后果是导出每轮都把
115
+ * 被覆盖的那几条重写一遍(面板上那个永远不归零的「已写 9」)、而反向同步会把一份内容
116
+ * 回灌给六条记录。
117
+ *
118
+ * 分配本身只取决于"基底 + 已占用集",因此**调用方必须按 id 排序后再分配**:
119
+ * 同一个基底碰上同一次分配时,谁先谁拿到裸名——顺序不钉死的话,两次导出会给同一条
120
+ * 记录分到不同名字,文件在盘上跳来跳去。后缀要留位置:总长不超过 {@link NAME_MAX},
121
+ * 宁可截基底也不让它超长。
122
+ *
123
+ * @param base - 基底名(通常是 `deriveName(content)`,可以为空)。
124
+ * @param id - 记录 id,基底不可用时落回 `mem-<id8>`。
125
+ * @param isFree - 库侧查询:这个名字没被**其它**记录占着。
126
+ * @param reserved - 本轮已分配出去的名字(调用方逐次加入,函数也会自己加)。
127
+ * @returns 可用的名字;实在找不到就把 `id` 的回落名加上数字尾巴交出去(仍进 reserved)。
128
+ */
129
+ export declare function allocUniqueName(base: string, id: string, isFree: (name: string) => boolean, reserved: Set<string>): string;