@thincoder/core 0.9.4 → 0.10.1
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/CHANGELOG.md +63 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +14 -14
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +23 -13
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +55 -294
- package/agent/family-tools.mjs +11 -11
- package/agent/helpers.mjs +63 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +19 -15
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +90 -30
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +11 -11
- package/agent-tools/advisor-settle.mjs +17 -17
- package/agent-tools/advisor.mjs +38 -43
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +23 -20
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +65 -34
- package/agent-tools/batch-paths.mjs +186 -0
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +22 -119
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +58 -58
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +18 -13
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +3 -3
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +29 -273
- package/agent-tools/subagent-async.mjs +36 -31
- package/agent-tools/subagent-panel.mjs +38 -9
- package/agent-tools/subagent-run.mjs +44 -29
- package/agent-tools/subagent-scheduler.mjs +29 -28
- package/agent-tools/subagent-spawn.mjs +57 -25
- package/agent-tools/subagent.mjs +46 -66
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +20 -351
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +12 -7
- package/config-watch.mjs +83 -0
- package/config.mjs +17 -4
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/git/checkpoint.mjs +4 -8
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +35 -7
- package/ledger-migrate.mjs +299 -0
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +11 -5
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-specs.mjs +131 -66
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +263 -0
- package/peer-domains.mjs +100 -71
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +19 -7
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +1 -2
- package/prompts/persona-engineering.md +41 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +10 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-gc.mjs +67 -6
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +90 -18
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -236
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/question.md +1 -0
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +10 -11
- package/tools/edit-diff.mjs +33 -25
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +11 -12
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/glob-dialect.mjs +3 -3
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/search.mjs +2 -2
- package/tools/shared.mjs +9 -7
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
package/context.mjs
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
import { chat } from "./provider/index.mjs"
|
|
15
15
|
import { buildCompressMessages } from "./compress-form.mjs"
|
|
16
|
-
import { contextUsage, estimateTokens,
|
|
16
|
+
import { contextUsage, estimateTokens, keepTailSize, splitHistory, tailBudgetTokens } from "./token-window.mjs"
|
|
17
|
+
import { COMPACTION_PLACEHOLDER, withPlaceholderPrefix } from "./context-echo.mjs"
|
|
18
|
+
import { shrinkOversized } from "./context-degrade.mjs"
|
|
17
19
|
|
|
18
20
|
export const SUMMARIZE_PROMPT = `The conversation above is our work log so far — summarize it into a compact summary for use as context in the ongoing conversation.
|
|
19
21
|
Requirements:
|
|
@@ -34,9 +36,6 @@ const COMPACTION_PREFIX =
|
|
|
34
36
|
"Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
|
|
35
37
|
"but re-verify transient state with tools. Check memory search for any missing decisions.]\n\n"
|
|
36
38
|
|
|
37
|
-
/** Placeholder assistant reply committed right after compaction (D9); D-CC18 merges it into an adjacent tail assistant instead of emitting it as a separate message */
|
|
38
|
-
const COMPACTION_PLACEHOLDER = "Understood. I'll continue from these notes, re-verifying anything transient."
|
|
39
|
-
|
|
40
39
|
/** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
|
|
41
40
|
export const COMPRESS_FAILURE_LIMIT = 3
|
|
42
41
|
|
|
@@ -71,115 +70,6 @@ function anchorText(agent) {
|
|
|
71
70
|
return lines.length > 0 ? lines.join("\n") : null
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
/**
|
|
75
|
-
* pushReal — the single entry point for REAL conversation messages.
|
|
76
|
-
* A real message (user input, assistant reply, tool result, multimodal image) is appended to BOTH:
|
|
77
|
-
* agent.history — the machine context (compaction shrinks this)
|
|
78
|
-
* agent._fullHistory — the human-readable record (persistence source)
|
|
79
|
-
* Machine-only messages ([System reminder:...], compaction notes, task/plan/checkpoint re-injections)
|
|
80
|
-
* are pushed directly to agent.history WITHOUT going through here, so they never enter _fullHistory.
|
|
81
|
-
* The two lines are written independently at the source — no after-the-fact delta sync.
|
|
82
|
-
* Message timestamps (SESSION.md §6.9): stamped HERE once at push time (epoch ms) — a single
|
|
83
|
-
* point covers every real message. Pre-existing ts (e.g. from another end writing the shared slot)
|
|
84
|
-
* is preserved; restored old messages keep no ts rather than getting a misleading backdate (D-S3).
|
|
85
|
-
* ts is a LOCAL-ONLY field — the send layer strips it before any provider request (T-S3).
|
|
86
|
-
*
|
|
87
|
-
* TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14)——人读线内存有界 + 磁盘为准:
|
|
88
|
-
* ① `agent._recordStore?.append(msg)`:记录同步追加(磁盘为准——append-only sidecar);
|
|
89
|
-
* ② 窗口驱逐:绑定态(agent._historyWindow = 200)下 _fullHistory 只保最近窗口条——
|
|
90
|
-
* 更早内容仅存磁盘(翻页/检索/保存从盘按需读)。未绑定(模式 F)不驱逐(零回归)。
|
|
91
|
-
* 追加失败不阻断回合(独立 try/catch——尽力面 N-S6;store 内部另置 degraded 并停写)。
|
|
92
|
-
*/
|
|
93
|
-
export function pushReal(agent, msg) {
|
|
94
|
-
if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
|
|
95
|
-
if (msg && msg.ts === undefined) msg.ts = Date.now()
|
|
96
|
-
agent._fullHistory.push(msg)
|
|
97
|
-
try { agent._recordStore?.append(msg) } catch { /* 尽力面:落盘失败不阻断回合(N-S6) */ }
|
|
98
|
-
const win = agent._historyWindow
|
|
99
|
-
if (win > 0 && agent._fullHistory.length > win) {
|
|
100
|
-
agent._fullHistory.splice(0, agent._fullHistory.length - win)
|
|
101
|
-
}
|
|
102
|
-
agent.history.push(msg)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Content-shape-safe prefixing (D-CC18, generalized for D-CC19 merge reuse):
|
|
107
|
-
* string → text + blank line + original; multimodal array → text part prepended;
|
|
108
|
-
* empty string / null / undefined / other → text alone.
|
|
109
|
-
*/
|
|
110
|
-
function prefixContent(content, text) {
|
|
111
|
-
if (typeof content === "string" && content.length > 0) return `${text}\n\n${content}`
|
|
112
|
-
if (Array.isArray(content)) return [{ type: "text", text }, ...content]
|
|
113
|
-
return text
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* D-CC18 echo safety: prefix the placeholder onto an existing assistant message's content.
|
|
118
|
-
* Thin wrapper over prefixContent (behavioral semantics unchanged).
|
|
119
|
-
*/
|
|
120
|
-
function withPlaceholderPrefix(content) {
|
|
121
|
-
return prefixContent(content, COMPACTION_PLACEHOLDER)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* D-CC19 restore-path echo merge (2026-09-16 ENGINE-DEBT 批 ED-1): persisted `contextHistory`
|
|
126
|
-
* is loaded back VERBATIM on session restore, so the D-CC18 pathological shape (an assistant
|
|
127
|
-
* WITHOUT reasoning_content directly followed by another assistant — DeepSeek-family thinking
|
|
128
|
-
* mode rejects the first request with 400) can revive from disk. Pure in-core function: scans
|
|
129
|
-
* only at restore time, never prompts the user, never rewrites the session file. Merge direction
|
|
130
|
-
* matches D-CC18 (the reasoning-less message is absorbed INTO its follower — the follower's
|
|
131
|
-
* tool_calls / reasoning_content / other fields are kept verbatim; texts joined with a blank
|
|
132
|
-
* line). Iterates to a fixed point (chains collapse in full). Copy-on-write: messages may be
|
|
133
|
-
* shared with other lines, so the merged message is always a NEW object; clean input returns
|
|
134
|
-
* the SAME array reference (zero copy).
|
|
135
|
-
*/
|
|
136
|
-
|
|
137
|
-
/** Pair predicate: prev = assistant with no/empty reasoning_content and no tool_calls,
|
|
138
|
-
* directly followed by another assistant. (Prev WITH tool_calls is never merged — pairing
|
|
139
|
-
* safety, F-3.) */
|
|
140
|
-
export function isAssistantEchoPair(prev, next) {
|
|
141
|
-
return prev?.role === "assistant"
|
|
142
|
-
&& next?.role === "assistant"
|
|
143
|
-
&& !prev.reasoning_content
|
|
144
|
-
&& !(Array.isArray(prev.tool_calls) && prev.tool_calls.length > 0)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Text of a message content in any supported shape (string / parts array / null-ish). */
|
|
148
|
-
function contentTextOf(content) {
|
|
149
|
-
if (typeof content === "string") return content
|
|
150
|
-
if (Array.isArray(content)) {
|
|
151
|
-
return content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("\n\n")
|
|
152
|
-
}
|
|
153
|
-
return ""
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Absorb prev's text into next's content (blank-line join; empty prev text → next unchanged). */
|
|
157
|
-
function absorbEchoContent(prevContent, nextContent) {
|
|
158
|
-
const text = contentTextOf(prevContent)
|
|
159
|
-
if (text.length === 0) return nextContent
|
|
160
|
-
return prefixContent(nextContent, text)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function mergeAdjacentAssistantEchoes(history) {
|
|
164
|
-
if (!Array.isArray(history)) return history
|
|
165
|
-
let src = history
|
|
166
|
-
for (;;) {
|
|
167
|
-
let changed = false
|
|
168
|
-
const next = []
|
|
169
|
-
for (let i = 0; i < src.length; i++) {
|
|
170
|
-
if (i + 1 < src.length && isAssistantEchoPair(src[i], src[i + 1])) {
|
|
171
|
-
next.push({ ...src[i + 1], content: absorbEchoContent(src[i].content, src[i + 1].content) })
|
|
172
|
-
i += 1
|
|
173
|
-
changed = true
|
|
174
|
-
} else {
|
|
175
|
-
next.push(src[i])
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (!changed) return src === history ? history : src
|
|
179
|
-
src = next
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
73
|
/** Replace middle with a note, then re-inject task/plan state (shared by LLM summary and truncation fallback) */
|
|
184
74
|
function applyCompression(agent, headEnd, tailStart, note) {
|
|
185
75
|
// _fullHistory already holds every real message (written at the source via pushReal),
|
|
@@ -314,7 +204,7 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
314
204
|
messages,
|
|
315
205
|
tools: extras?.tools,
|
|
316
206
|
signal,
|
|
317
|
-
// §
|
|
207
|
+
// TRACES.md §6.1 D-TR4:轨迹元数据增补——kind=compress(上下文构建面——agent 元数据透出;
|
|
318
208
|
// depth 经 extras.traceDepth——agent.mjs 主作用域传入——compress 调用点补齐)
|
|
319
209
|
logCtx: {
|
|
320
210
|
stage: "compress", child: agent._logId, kind: "compress",
|
|
@@ -342,37 +232,6 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
342
232
|
return true
|
|
343
233
|
}
|
|
344
234
|
|
|
345
|
-
/** prune stub 逐字(§6.16.3):只给「已清理 + 原长度 + 重跑路径」——prune 是**删**不是摘要,不声称可复原。 */
|
|
346
|
-
const pruneStub = (chars) => `[pruned: stale tool output dropped (${chars} chars) — re-run the tool if you need it again.]`
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* 陈旧工具输出清理(F-CC3 · §6.16.3):合格集 = `token-window.mjs` 单源(保护尾外 ∧ `role:"tool"` ∧ ≥ 门槛);
|
|
350
|
-
* 命中项**原位换「内容」**(`history[i] = { ...m, content: stub }`——数组引用 / 长度 / 索引 / `tool_call_id` 全不变
|
|
351
|
-
* ⇒ 配对**结构上不可能被拆**)。记录面零改(copy-on-write:消息对象与人读线共享);基线失效同 `shrinkOversized` 先例。
|
|
352
|
-
* @returns {{pruned:number, freed:number, candidates:number, tailKept:number, belowMin:number}}
|
|
353
|
-
*/
|
|
354
|
-
export function pruneStaleToolOutputs(agent) {
|
|
355
|
-
const history = agent.history
|
|
356
|
-
const stale = collectStaleToolOutputs(history, agent.provider)
|
|
357
|
-
const counts = {
|
|
358
|
-
pruned: stale.indexes.length,
|
|
359
|
-
freed: stale.tokens,
|
|
360
|
-
candidates: stale.candidates,
|
|
361
|
-
tailKept: stale.tailKept,
|
|
362
|
-
belowMin: stale.belowMin,
|
|
363
|
-
}
|
|
364
|
-
if (counts.pruned === 0) return counts
|
|
365
|
-
for (const i of stale.indexes) {
|
|
366
|
-
const m = history[i]
|
|
367
|
-
// 多模态 tool 结果(content 数组)整体替换为 stub ⇒ 图像 part 丢弃(不可再取——prune 是删;回执只给重跑路径)
|
|
368
|
-
const chars = typeof m.content === "string" ? m.content.length : JSON.stringify(m.content ?? "").length
|
|
369
|
-
history[i] = { ...m, content: pruneStub(chars) }
|
|
370
|
-
}
|
|
371
|
-
agent._lastPromptTokens = null
|
|
372
|
-
agent._usageAtLen = null
|
|
373
|
-
return counts
|
|
374
|
-
}
|
|
375
|
-
|
|
376
235
|
/**
|
|
377
236
|
* Deterministic truncation fallback: called when the summary LLM fails repeatedly, no network call.
|
|
378
237
|
* Drops the middle so the task can continue. Returns whether truncation happened.
|
|
@@ -389,50 +248,12 @@ export function compressFallback(agent) {
|
|
|
389
248
|
return true
|
|
390
249
|
}
|
|
391
250
|
|
|
392
|
-
/** Hard truncation limit for a single message body: when exceeded and the splitter can't find a middle section, truncate to a stub (prevents one giant message from blocking compaction) */
|
|
393
|
-
const OVERSIZE_CONTENT_LIMIT = 8_000
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Deterministic shrinking: last resort when splitHistory can't find a middle section (history too short) but threshold is exceeded. No LLM call.
|
|
397
|
-
* Truncates user/tool message bodies exceeding OVERSIZE_CONTENT_LIMIT to a stub (keeps head + tail);
|
|
398
|
-
* does not touch reasoning_content (DeepSeek/Kimi echo protocol) or tool_calls pairing structure — no protocol 400
|
|
399
|
-
* risk from this path (this module's echo-safety face is `applyCompression`: the compaction placeholder is never
|
|
400
|
-
* committed as a separate reasoning-less assistant next to another assistant — D-CC18, 2026-09-16 batch).
|
|
401
|
-
* Only called after compressIfNeeded determines threshold is exceeded. Returns whether any message was truncated.
|
|
402
|
-
*/
|
|
403
|
-
function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
|
|
404
|
-
let shrunk = false
|
|
405
|
-
// Copy-on-write: build a NEW array and replace only truncated entries. pushReal stores the SAME
|
|
406
|
-
// message object in both `agent.history` (machine line) and `agent._fullHistory` (human/persistence
|
|
407
|
-
// line), so in-place `m.content = ...` would ALSO truncate the never-compacted human line and lose
|
|
408
|
-
// the original pasted content on session persist (session.mjs persists _fullHistory). VS Code port
|
|
409
|
-
// already copies (`history.map(m => ({ ...m }))`); this brings CLI to parity.
|
|
410
|
-
const next = agent.history.map((m) => {
|
|
411
|
-
if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") return m
|
|
412
|
-
if (m.content.length <= limit) return m
|
|
413
|
-
// Truncate keeping head + tail, insert stub in between; keepHead/keepTail proportional but not exceeding 50%/25% of limit
|
|
414
|
-
const keepHead = Math.min(Math.floor(limit * 0.5), 4000)
|
|
415
|
-
const keepTail = Math.min(Math.floor(limit * 0.25), 2000)
|
|
416
|
-
shrunk = true
|
|
417
|
-
return {
|
|
418
|
-
...m,
|
|
419
|
-
content:
|
|
420
|
-
m.content.slice(0, keepHead) +
|
|
421
|
-
`\n[... ${m.content.length - keepHead - keepTail} chars truncated — single message too large for context window ...]\n` +
|
|
422
|
-
m.content.slice(-keepTail),
|
|
423
|
-
}
|
|
424
|
-
})
|
|
425
|
-
if (shrunk) {
|
|
426
|
-
agent.history = next
|
|
427
|
-
// Same as compaction: measured token baseline is invalidated by the changed history, fall back to estimation until next response
|
|
428
|
-
agent._lastPromptTokens = null
|
|
429
|
-
agent._usageAtLen = null
|
|
430
|
-
}
|
|
431
|
-
return shrunk
|
|
432
|
-
}
|
|
433
|
-
|
|
434
251
|
// ─── 迁出面(import 面保持:TUI / verify-compress / VSC 对拍经本档取——先例 = 下方 explore-distill 再导出)──
|
|
435
252
|
export { estimateTokens }
|
|
253
|
+
// ─── 拆分转口(2026-10-01 拆分批 · #755 ∥ #786):写缝 ∥ 回声 ∥ 降级三面外提——消费面 / 批内件 import 面零改 ───
|
|
254
|
+
export { pushReal, pushRecord } from "./context-push.mjs"
|
|
255
|
+
export { isAssistantEchoPair, mergeAdjacentAssistantEchoes } from "./context-echo.mjs"
|
|
256
|
+
export { pruneStaleToolOutputs } from "./context-degrade.mjs"
|
|
436
257
|
// ─── End-of-run exploration distillation(2026-09-05 module-split:524 > 500 硬限——verbatim
|
|
437
258
|
// 迁至 explore-distill.mjs,语义零变——VS Code compact.mjs 同款联动;cross-repo parity 锚改指
|
|
438
259
|
// explore-distill.mjs——消费方 import 面不变(re-export))───────────────────────
|
package/conventions.mjs
CHANGED
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* conventions.mjs — the single authority for code / doc / temp path classification,
|
|
3
|
-
* plus the project
|
|
2
|
+
* conventions.mjs — the single authority for code / doc / temp / aux path classification,
|
|
3
|
+
* plus the project declaration surface (the three declaration families of the project's
|
|
4
|
+
* `PROJECT-MANIFEST.json`: `codePaths` / `index.*` / `advisor.*`).
|
|
4
5
|
*
|
|
5
|
-
* Why one module:
|
|
6
|
+
* Why one module: the write-domain gates and guards (design gate, review-doc gate,
|
|
6
7
|
* mutation accounting, verify fast path) each carried their own copy of the
|
|
7
8
|
* "what counts as product code" predicate — anchored `^src/` regexes, `docs/`
|
|
8
9
|
* prefix checks, component regexes. Each copy drifted, and each hardcoded THIS
|
|
9
10
|
* repository's layout: a project whose code lives outside `src/` slipped through
|
|
10
|
-
* the design gate silently (PORTABILITY FR12 / PO-10). One classifier + one
|
|
11
|
-
*
|
|
11
|
+
* the design gate silently (PORTABILITY FR12 / PO-10). One classifier + one declared
|
|
12
|
+
* carrier = one truth. It is a **shared** classification surface: the gates are consumers,
|
|
13
|
+
* not owners (consumer faces: advisor / agent-tools / both ends).
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Declaration (2026-09-27 · `.thincoder/conventions.json` retired — single carrier):
|
|
16
|
+
* the three families live in the project manifest and are projected through `manifest.mjs`
|
|
17
|
+
* → `declaration.mjs` (declaration loading split out of this file 2026-10-01 — this file keeps the
|
|
18
|
+
* classification adjudication; ONE reader / ONE root resolution — `readManifest` + `manifestFilePath`;
|
|
19
|
+
* name / shape / defaults are schema-owned by `docs/core/design/MANIFEST.md` §2.2).
|
|
20
|
+
* Missing manifest → pure defaults (silent); unusable manifest (invalid / read error) →
|
|
21
|
+
* defaults + console.warn + a log event (never crash, never swallow — PORTABILITY FR10/D16).
|
|
22
|
+
* A retired `.thincoder/conventions.json` is never read: while present it yields ONE
|
|
23
|
+
* visible warning per load (existence check only — zero content parsing — KD-M1-34).
|
|
17
24
|
*
|
|
18
|
-
* Classification vocabulary (PORTABILITY design §3.
|
|
19
|
-
* code — inside a declared code segment (default: the path segment `src`), or
|
|
20
|
-
*
|
|
21
|
-
*
|
|
25
|
+
* Classification vocabulary (PORTABILITY design §3.2):
|
|
26
|
+
* code — inside a declared code segment (default: the path segment `src`), or the
|
|
27
|
+
* fallback; doc — documentation extension outside any code segment and not
|
|
28
|
+
* scratch; temp — tmp-* name or .tmp/.temp extension; aux — a default
|
|
29
|
+
* auxiliary-path segment sequence (test / tests / scripts / .thincoder/tmp —
|
|
30
|
+
* F9) that matched no code segment and neither doc nor temp.
|
|
31
|
+
* Precedence: code segment → temp → doc → aux → fallback code.
|
|
32
|
+
*
|
|
33
|
+
* Segment matching runs on the PROJECT-ROOT-RELATIVE FACE of a path (D17 · 2026-09-28): the
|
|
34
|
+
* machine layout ABOVE the project root never participates. An absolute path outside the
|
|
35
|
+
* root / an unknown root / a `.`-`..` jump segment ⇒ the face is indeterminate: code segments
|
|
36
|
+
* fall back to the whole path (the interception surface does not shrink), aux sequences do
|
|
37
|
+
* NOT match (the former fail-open edge — an ancestor segment exempted a whole subtree).
|
|
22
38
|
*/
|
|
23
|
-
import {
|
|
24
|
-
import { join, resolve } from "node:path"
|
|
25
|
-
import { logEvent } from "./log.mjs"
|
|
26
|
-
|
|
27
|
-
/** Default code-path segments (data, not logic — a project may replace them). */
|
|
28
|
-
export const DEFAULT_CODE_PATHS = ["src"]
|
|
39
|
+
import { DEFAULT_DECLARATION } from "./declaration.mjs"
|
|
29
40
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
41
|
+
/** Default auxiliary-path segment sequences (data, not logic — F9): the engineering-mode
|
|
42
|
+
* parent gate's default exemption surface — `test` / `tests` / `scripts` at any depth,
|
|
43
|
+
* plus the `.thincoder/tmp` scratch space (two-segment sequence). Not declarable — a
|
|
44
|
+
* project reclaims a sequence by listing it in `codePaths` (code segment wins). */
|
|
45
|
+
export const DEFAULT_AUX_PATHS = ["test", "tests", "scripts", ".thincoder/tmp"]
|
|
32
46
|
|
|
33
47
|
/** Documentation predicate (moved here verbatim from advisor/repos.mjs — one copy). */
|
|
34
48
|
const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
|
|
@@ -46,18 +60,44 @@ function segmentsOf(p) {
|
|
|
46
60
|
return String(p ?? "").replace(/\\/g, "/").split("/").filter(Boolean)
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
const lowerSeg = (s) => s.toLowerCase()
|
|
64
|
+
const isJumpSeg = (seg) => seg === "." || seg === ".."
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The segment-matching face of a path — its position INSIDE the project (PORTABILITY design
|
|
68
|
+
* §3.2 · D17「段匹配面 = 项目根相对面」):
|
|
69
|
+
* · absolute form under the root (separators normalized + per-segment case-insensitive
|
|
70
|
+
* comparison) ⇒ the REMAINING segments (an EMPTY list when the path IS the root);
|
|
71
|
+
* · relative form ⇒ its own segments;
|
|
72
|
+
* · anything else — outside the root / root unknown / a `.` / `..` jump segment ⇒
|
|
73
|
+
* `null` = indeterminate.
|
|
74
|
+
*/
|
|
75
|
+
function faceOf(p, root) {
|
|
76
|
+
const s = String(p ?? "").replace(/\\/g, "/")
|
|
77
|
+
const segs = segmentsOf(s)
|
|
78
|
+
if (!/^([A-Za-z]:|\/)/.test(s)) return segs.some(isJumpSeg) ? null : segs.map(lowerSeg)
|
|
79
|
+
if (typeof root !== "string" || root === "") return null
|
|
80
|
+
const rootSegs = segmentsOf(root).map(lowerSeg)
|
|
81
|
+
if (segs.length < rootSegs.length) return null
|
|
82
|
+
for (let i = 0; i < rootSegs.length; i++) {
|
|
83
|
+
if (lowerSeg(segs[i]) !== rootSegs[i]) return null
|
|
84
|
+
}
|
|
85
|
+
const rest = segs.slice(rootSegs.length)
|
|
86
|
+
if (rest.length === 0) return []
|
|
87
|
+
return rest.some(isJumpSeg) ? null : rest.map(lowerSeg)
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
/**
|
|
50
|
-
* True when
|
|
91
|
+
* True when `segments` contain any of the given segment sequences at any depth.
|
|
51
92
|
* Segment matching (not a prefix anchor) is what closes the nested-layout hole:
|
|
52
|
-
* `packages/foo/src/x.md`
|
|
53
|
-
* case-insensitive — on case-insensitive filesystems
|
|
54
|
-
* directory, and the gate must not be bypassable by casing.
|
|
93
|
+
* `packages/foo/src/x.md` matches the sequence ["src"] — product code, not a
|
|
94
|
+
* document. Comparison is case-insensitive — on case-insensitive filesystems
|
|
95
|
+
* `Src/x.mjs` is the same directory, and the gate must not be bypassable by casing.
|
|
55
96
|
*/
|
|
56
|
-
function
|
|
57
|
-
const parts =
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const want = segmentsOf(entry).map((s) => s.toLowerCase())
|
|
97
|
+
function hasSequenceIn(segments, sequences) {
|
|
98
|
+
const parts = segments.map(lowerSeg)
|
|
99
|
+
for (const entry of sequences) {
|
|
100
|
+
const want = segmentsOf(entry).map(lowerSeg)
|
|
61
101
|
if (want.length === 0) continue
|
|
62
102
|
for (let i = 0; i + want.length <= parts.length; i++) {
|
|
63
103
|
if (want.every((seg, j) => parts[i + j] === seg)) return true
|
|
@@ -66,15 +106,40 @@ function hasCodeSegment(p, conv) {
|
|
|
66
106
|
return false
|
|
67
107
|
}
|
|
68
108
|
|
|
69
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Segment matching on the project-root-relative face (D17). An indeterminate face splits the
|
|
111
|
+
* two callers by design: code segments fall back to the WHOLE path (`fallbackAll` — the
|
|
112
|
+
* interception surface does not shrink), while aux sequences do NOT match (the former
|
|
113
|
+
* fail-open edge: an ancestor segment above the root used to exempt a whole subtree).
|
|
114
|
+
*/
|
|
115
|
+
function hasSegmentSequence(p, sequences, root, fallbackAll) {
|
|
116
|
+
const face = faceOf(p, root)
|
|
117
|
+
if (face === null) return fallbackAll ? hasSequenceIn(segmentsOf(p), sequences) : false
|
|
118
|
+
return hasSequenceIn(face, sequences)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** True when the path contains a declared code segment sequence (on the face — D17). */
|
|
122
|
+
function hasCodeSegment(p, conv) {
|
|
123
|
+
return hasSegmentSequence(p, conv?.codePaths ?? DEFAULT_DECLARATION.codePaths, conv?.root, true)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** True when the path contains a default auxiliary-path segment sequence (F9 · face-bound). */
|
|
127
|
+
function hasAuxSegment(p, conv) {
|
|
128
|
+
return hasSegmentSequence(p, DEFAULT_AUX_PATHS, conv?.root, false)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** "code" | "doc" | "temp" | "aux" — the single classification decision.
|
|
70
132
|
* Precedence: code segment first (src/** stays product code even when the name
|
|
71
133
|
* looks scratch — the pre-existing unconditional-src rule), then temp, then a
|
|
72
|
-
* documentation extension,
|
|
134
|
+
* documentation extension, then an auxiliary-path segment sequence (test / tests
|
|
135
|
+
* / scripts / .thincoder/tmp — F9), else code (anything not doc/temp/aux is product
|
|
136
|
+
* code). Both segment checks run on the project-root-relative face (D17 — `faceOf`). */
|
|
73
137
|
export function classifyPath(p, conv) {
|
|
74
138
|
const s = String(p ?? "")
|
|
75
139
|
if (hasCodeSegment(s, conv)) return "code"
|
|
76
140
|
if (TEMP_FILE.test(s)) return "temp"
|
|
77
141
|
if (DOC_FILE.test(s)) return "doc"
|
|
142
|
+
if (hasAuxSegment(s, conv)) return "aux"
|
|
78
143
|
return "code"
|
|
79
144
|
}
|
|
80
145
|
|
|
@@ -90,134 +155,18 @@ export function isDocPath(p, conv) {
|
|
|
90
155
|
return DOC_FILE.test(s) && !hasCodeSegment(s, conv)
|
|
91
156
|
}
|
|
92
157
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
for (const e of v) {
|
|
102
|
-
if (typeof e !== "string") continue
|
|
103
|
-
const t = e.trim().toLowerCase()
|
|
104
|
-
if (!t) continue
|
|
105
|
-
const ext = t.startsWith(".") ? t : `.${t}`
|
|
106
|
-
if (!out.includes(ext)) out.push(ext)
|
|
107
|
-
}
|
|
108
|
-
return out
|
|
158
|
+
/** True when the path is an auxiliary path (`classifyPath === "aux"`) — the engineering-mode
|
|
159
|
+
* parent gate's default exemption surface (test / tests / scripts at any depth ·
|
|
160
|
+
* .thincoder/tmp/** — matched on the project-root-relative face only, D17). Not declarable:
|
|
161
|
+
* a project reclaims one by listing its sequence in `codePaths`, which then wins (code
|
|
162
|
+
* segment first). Production faces read aux through classifyPath / isCodePath; this
|
|
163
|
+
* predicate is the readable form for the test face. */
|
|
164
|
+
export function isAuxPath(p, conv) {
|
|
165
|
+
return classifyPath(p, conv) === "aux"
|
|
109
166
|
}
|
|
110
167
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (typeof e !== "string") continue
|
|
117
|
-
const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
|
|
118
|
-
if (!s || out.includes(s)) continue
|
|
119
|
-
out.push(s)
|
|
120
|
-
}
|
|
121
|
-
return out.length > 0 ? out : null
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function normalizeString(v) {
|
|
125
|
-
return typeof v === "string" && v.trim() ? v.trim() : ""
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Per-key type check for recognized keys (present but wrong type). §3.2/§4.1: a
|
|
129
|
-
* type error degrades WITH a warning — never silently (the fallback semantics stay
|
|
130
|
-
* per-key; only the visibility is added here). */
|
|
131
|
-
function typeErrorsOf(raw) {
|
|
132
|
-
const isObj = (v) => v !== undefined && v !== null && typeof v === "object" && !Array.isArray(v)
|
|
133
|
-
const strArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string")
|
|
134
|
-
const errs = []
|
|
135
|
-
if (raw.codePaths !== undefined && !strArray(raw.codePaths)) errs.push("codePaths must be an array of strings")
|
|
136
|
-
const idx = raw.index
|
|
137
|
-
if (idx !== undefined && !isObj(idx)) errs.push("index must be an object")
|
|
138
|
-
else if (isObj(idx)) {
|
|
139
|
-
for (const k of ["codeExtensions", "docExtensions"]) {
|
|
140
|
-
if (idx[k] !== undefined && !strArray(idx[k])) errs.push(`index.${k} must be an array of strings`)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const adv = raw.advisor
|
|
144
|
-
if (adv !== undefined && !isObj(adv)) errs.push("advisor must be an object")
|
|
145
|
-
else if (isObj(adv)) {
|
|
146
|
-
for (const k of ["docMap", "standardsDoc"]) {
|
|
147
|
-
if (adv[k] !== undefined && typeof adv[k] !== "string") errs.push(`advisor.${k} must be a string`)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return errs
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function buildConventions(raw) {
|
|
154
|
-
const codePaths = normalizeCodePaths(raw?.codePaths)
|
|
155
|
-
const codeExtensions = normalizeExtensions(raw?.index?.codeExtensions)
|
|
156
|
-
const docExtensions = normalizeExtensions(raw?.index?.docExtensions)
|
|
157
|
-
const docMap = normalizeString(raw?.advisor?.docMap)
|
|
158
|
-
const standardsDoc = normalizeString(raw?.advisor?.standardsDoc)
|
|
159
|
-
// `declared` = the declaration actually took effect (at least one recognized key
|
|
160
|
-
// honored) — the design-gate hint reads it to decide whether to point at the
|
|
161
|
-
// declaration file ("declare project conventions … to adjust").
|
|
162
|
-
const declared = Boolean(codePaths || codeExtensions.length || docExtensions.length || docMap || standardsDoc)
|
|
163
|
-
return Object.freeze({
|
|
164
|
-
declared,
|
|
165
|
-
codePaths: Object.freeze(codePaths ?? [...DEFAULT_CODE_PATHS]),
|
|
166
|
-
index: Object.freeze({
|
|
167
|
-
codeExtensions: Object.freeze(codeExtensions),
|
|
168
|
-
docExtensions: Object.freeze(docExtensions),
|
|
169
|
-
}),
|
|
170
|
-
advisor: Object.freeze({ docMap, standardsDoc }),
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Full-default conventions (no declaration) — the fallback every consumer gets. */
|
|
175
|
-
export const DEFAULT_CONVENTIONS = buildConventions(null)
|
|
176
|
-
|
|
177
|
-
const _cache = new Map()
|
|
178
|
-
|
|
179
|
-
/** Drop the per-root cache (test seam — declaration files are read once per root). */
|
|
180
|
-
export function clearConventionsCache() {
|
|
181
|
-
_cache.clear()
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Load (and cache) the normalized conventions for a project root.
|
|
186
|
-
* @param {string} cwd — project root (declaration lives at .thincoder/conventions.json)
|
|
187
|
-
* @returns {Readonly<{declared: boolean, codePaths: readonly string[],
|
|
188
|
-
* index: {codeExtensions: string[], docExtensions: string[]},
|
|
189
|
-
* advisor: {docMap: string, standardsDoc: string}}>}
|
|
190
|
-
*/
|
|
191
|
-
export function loadConventions(cwd) {
|
|
192
|
-
const root = resolve(cwd ?? process.cwd())
|
|
193
|
-
const hit = _cache.get(root)
|
|
194
|
-
if (hit) return hit
|
|
195
|
-
let conv = DEFAULT_CONVENTIONS
|
|
196
|
-
try {
|
|
197
|
-
const text = readFileSync(join(root, CONVENTIONS_REL_PATH), "utf8")
|
|
198
|
-
try {
|
|
199
|
-
const raw = JSON.parse(text)
|
|
200
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("top level must be a JSON object")
|
|
201
|
-
conv = buildConventions(raw)
|
|
202
|
-
const typeErrs = typeErrorsOf(raw)
|
|
203
|
-
if (typeErrs.length > 0) {
|
|
204
|
-
// Wrong-typed keys fall back per-key — but the user must SEE that their
|
|
205
|
-
// declaration did not take effect (never silently swallowed).
|
|
206
|
-
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} has invalid value types (${typeErrs.join("; ")}) — those keys fall back to defaults`)
|
|
207
|
-
logEvent("conventions:error", { cwd: root, err: `type errors: ${typeErrs.join("; ").slice(0, 160)}` })
|
|
208
|
-
}
|
|
209
|
-
} catch (e) {
|
|
210
|
-
// Corrupt file / wrong shape → defaults, visible: warn + event (never silent).
|
|
211
|
-
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} unreadable (${e?.message ?? e}) — falling back to defaults`)
|
|
212
|
-
logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
|
|
213
|
-
}
|
|
214
|
-
} catch (e) {
|
|
215
|
-
if (e?.code !== "ENOENT") {
|
|
216
|
-
// File exists but cannot be read (EACCES etc.) — same visible degradation.
|
|
217
|
-
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} not readable (${e?.message ?? e}) — falling back to defaults`)
|
|
218
|
-
logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
_cache.set(root, conv)
|
|
222
|
-
return conv
|
|
223
|
-
}
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// 迁出面(2026-10-01 拆分批 · #755 ∥ #786):声明装载面外提 `declaration.mjs`——本档经
|
|
170
|
+
// 转口保名(消费面 / 批内件 import 面零改;`DEFAULT_DECLARATION` 另供本档分类裁判缺省取用)。
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
export { loadProjectDeclaration, DEFAULT_DECLARATION, isExcludedRelPath, declaredPublicRoots, clearDeclarationCache } from "./declaration.mjs"
|