@thincoder/core 0.9.5 → 0.10.2
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 +49 -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 +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- 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 +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -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 +15 -11
- 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 +89 -29
- 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 +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- 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 +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- 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 +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- 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 +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +29 -12
- 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/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 +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- 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-ref.mjs +96 -3
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- 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 +16 -8
- 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 +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -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-guard.mjs +27 -7
- package/session-lifecycle.mjs +108 -27
- 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 -241
- 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/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 +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- 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/shared.mjs +8 -6
- 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/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"
|
package/declaration.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* declaration.mjs — 声明装载面(PROJECT-MANIFEST.json 三族 `codePaths` / `index.*` / `advisor.*`)
|
|
3
|
+
* · 自 `conventions.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786。
|
|
4
|
+
* 投影单源 = `manifest.mjs`(ONE reader / ONE root resolution——`readManifest` + `manifestFilePath`;
|
|
5
|
+
* name / shape / defaults = schema 面 `docs/core/design/MANIFEST.md` §2.2)。迁出块逐字;
|
|
6
|
+
* 原档 `conventions.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改)。
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from "node:fs"
|
|
9
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
10
|
+
import { DEFAULT_MANIFEST, manifestFilePath, readManifest } from "./manifest.mjs"
|
|
11
|
+
import { logEvent } from "./log.mjs"
|
|
12
|
+
|
|
13
|
+
/** Retired declaration carrier (project-root relative). Its EXISTENCE is checked, its
|
|
14
|
+
* content is never read — the declaration surface = the manifest three families (KD-M1-34). */
|
|
15
|
+
const RETIRED_REL_PATH = ".thincoder/conventions.json"
|
|
16
|
+
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Declaration loading — the three families of PROJECT-MANIFEST.json, projected
|
|
19
|
+
// through manifest.mjs (ONE reader / ONE root resolution) and cached per DATA-FILE
|
|
20
|
+
// PATH (`clearDeclarationCache()` is the test seam; declarations change rarely).
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function normalizeExtensions(v) {
|
|
24
|
+
if (!Array.isArray(v)) return []
|
|
25
|
+
const out = []
|
|
26
|
+
for (const e of v) {
|
|
27
|
+
if (typeof e !== "string") continue
|
|
28
|
+
const t = e.trim().toLowerCase()
|
|
29
|
+
if (!t) continue
|
|
30
|
+
const ext = t.startsWith(".") ? t : `.${t}`
|
|
31
|
+
if (!out.includes(ext)) out.push(ext)
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Declared code paths replace the default (replacement, not union — §3.1). An EMPTY
|
|
37
|
+
* array is a legal declaration ("no segment-based code face") — only a non-array value
|
|
38
|
+
* falls back to `null` (⇒ caller keeps the default). */
|
|
39
|
+
function normalizeCodePaths(v) {
|
|
40
|
+
if (!Array.isArray(v)) return null
|
|
41
|
+
const out = []
|
|
42
|
+
for (const e of v) {
|
|
43
|
+
if (typeof e !== "string") continue
|
|
44
|
+
const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
|
|
45
|
+
if (!s || out.includes(s)) continue
|
|
46
|
+
out.push(s)
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 路径列表归一(单源——`index.excludePaths` §6.14 ∥ `index.publicRepos` §6.15 同款):首部 `./` 剥离 · `\`→`/` · 去尾斜杠 · 空 ∕ 非串剔除 · 去重保序。 */
|
|
52
|
+
function normalizePathList(v) {
|
|
53
|
+
if (!Array.isArray(v)) return []
|
|
54
|
+
const out = []
|
|
55
|
+
for (const e of v) {
|
|
56
|
+
if (typeof e !== "string") continue
|
|
57
|
+
const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
|
|
58
|
+
if (!s || out.includes(s)) continue
|
|
59
|
+
out.push(s)
|
|
60
|
+
}
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeString(v) {
|
|
65
|
+
return typeof v === "string" && v.trim() ? v.trim() : ""
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Project the manifest three families into the frozen declaration object.
|
|
69
|
+
* `declared` = VALUE comparison against the defaults — at least one family deviates
|
|
70
|
+
* (a key written with its default value still counts as NOT declared; the design-gate
|
|
71
|
+
* hint reads it to decide whether to point at the manifest, PORTABILITY §3.1).
|
|
72
|
+
* `root` = the project root this declaration belongs to (`dirname(manifestFilePath(cwd))` —
|
|
73
|
+
* the segment-matching face's anchor, D18); NOT a declared value (excluded from `declared`);
|
|
74
|
+
* `null` = unknown root (`DEFAULT_DECLARATION`). */
|
|
75
|
+
function buildDeclaration(m, root = null) {
|
|
76
|
+
const codePaths = normalizeCodePaths(m?.codePaths) ?? [...DEFAULT_MANIFEST.codePaths]
|
|
77
|
+
const codeExtensions = normalizeExtensions(m?.index?.codeExtensions)
|
|
78
|
+
const docExtensions = normalizeExtensions(m?.index?.docExtensions)
|
|
79
|
+
const excludePaths = normalizePathList(m?.index?.excludePaths)
|
|
80
|
+
const publicRepos = normalizePathList(m?.index?.publicRepos)
|
|
81
|
+
const docMap = normalizeString(m?.advisor?.docMap)
|
|
82
|
+
const standardsDoc = normalizeString(m?.advisor?.standardsDoc)
|
|
83
|
+
const defaults = normalizeCodePaths(DEFAULT_MANIFEST.codePaths) ?? []
|
|
84
|
+
const pathsDiffer = codePaths.length !== defaults.length || codePaths.some((p, i) => p !== defaults[i])
|
|
85
|
+
const declared = pathsDiffer || codeExtensions.length > 0 || docExtensions.length > 0 || excludePaths.length > 0 || publicRepos.length > 0 || Boolean(docMap) || Boolean(standardsDoc)
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
declared,
|
|
88
|
+
codePaths: Object.freeze(codePaths),
|
|
89
|
+
index: Object.freeze({
|
|
90
|
+
codeExtensions: Object.freeze(codeExtensions),
|
|
91
|
+
docExtensions: Object.freeze(docExtensions),
|
|
92
|
+
excludePaths: Object.freeze(excludePaths),
|
|
93
|
+
publicRepos: Object.freeze(publicRepos),
|
|
94
|
+
}),
|
|
95
|
+
advisor: Object.freeze({ docMap, standardsDoc }),
|
|
96
|
+
root,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Full-default declaration (no manifest / unusable manifest) — the fallback every
|
|
101
|
+
* consumer gets; built from `DEFAULT_MANIFEST` so the default values stay single-source. */
|
|
102
|
+
export const DEFAULT_DECLARATION = buildDeclaration(null)
|
|
103
|
+
|
|
104
|
+
/** 排除前缀谓词(单源——§6.14 面①):`rel`(**`base` 相对**——缺省视为根相对)命中声明前缀(恰等
|
|
105
|
+
* ∨ 后随 `/`——`openclaw` 不吞 `openclaw-fork`)⇒ true;缺省 `[]` ⇒ 恒 false(零行为变化)。
|
|
106
|
+
* `base`(调用面 cwd/origin·#700)= `rel` 的相对基:`resolve` + `relative` 换算至 `decl.root` 根面
|
|
107
|
+
* 再比前缀(cwd = 项目根时与既有行为逐字等义);换算越出根面(`..` 头 ∥ 根外绝对形)⇒ **不命中**
|
|
108
|
+
* (保守不排除——沉默洞方向收窄,与 D17 aux 面「indeterminate ⇒ 不匹配」同向);`decl.root` 缺
|
|
109
|
+
* ⇒ `rel` 原样(根起步调用面等义)。 */
|
|
110
|
+
export function isExcludedRelPath(rel, decl, base = null) {
|
|
111
|
+
const list = decl?.index?.excludePaths
|
|
112
|
+
if (!Array.isArray(list) || list.length === 0) return false
|
|
113
|
+
let s = String(rel ?? "").replace(/\\/g, "/")
|
|
114
|
+
const root = decl?.root
|
|
115
|
+
if (base != null && root) {
|
|
116
|
+
const converted = relative(root, resolve(base, s)).replace(/\\/g, "/")
|
|
117
|
+
if (converted === ".." || converted.startsWith("../") || isAbsolute(converted)) return false
|
|
118
|
+
s = converted
|
|
119
|
+
}
|
|
120
|
+
return list.some((p) => p && (s === p || s.startsWith(`${p}/`)))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 声明公共仓**绝对根集**(§6.15 解析层——检索同步 ∥ 读面 origin 集 ∥ 引用解析三消费面共用的单一定义,
|
|
125
|
+
* 不得二写):值形层逐项 `resolve(decl.root, p)` ⇒ 绝对根集(去重保序);存在性判归消费面(本层不判)。
|
|
126
|
+
* 缺省 `[]` ⇒ `[]`(零行为)。
|
|
127
|
+
* @param {string} [cwd] 会话锚(项目根经 manifest.mjs 解析)
|
|
128
|
+
* @returns {string[]} 绝对路径(去重保序)
|
|
129
|
+
*/
|
|
130
|
+
export function declaredPublicRoots(cwd) {
|
|
131
|
+
const decl = loadProjectDeclaration(cwd)
|
|
132
|
+
const list = decl?.index?.publicRepos
|
|
133
|
+
if (!Array.isArray(list) || list.length === 0) return []
|
|
134
|
+
const base = typeof decl.root === "string" && decl.root !== "" ? decl.root : resolve(cwd ?? ".")
|
|
135
|
+
const out = []
|
|
136
|
+
for (const p of list) {
|
|
137
|
+
const abs = resolve(base, p)
|
|
138
|
+
if (!out.includes(abs)) out.push(abs)
|
|
139
|
+
}
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const _cache = new Map()
|
|
144
|
+
|
|
145
|
+
/** Drop the per-data-file-path cache (test seam — declarations are read once per path). */
|
|
146
|
+
export function clearDeclarationCache() {
|
|
147
|
+
_cache.clear()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Retired-carrier warning (existence check only — zero content parsing, zero fallback). */
|
|
151
|
+
function warnRetiredCarrier(root) {
|
|
152
|
+
console.warn(`[declaration] ${RETIRED_REL_PATH} is retired and no longer read — move codePaths / index.*Extensions / advisor.{docMap,standardsDoc} into PROJECT-MANIFEST.json`)
|
|
153
|
+
logEvent("declaration:retired-file", { cwd: root })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Load (and cache) the normalized project declaration for a project.
|
|
158
|
+
* The data file is located through `manifestFilePath(cwd)` (ONE root resolution — a cwd
|
|
159
|
+
* deeper than the project root still reads the project's manifest) and read through
|
|
160
|
+
* `readManifest` (ONE reader — this module carries no second parse of the manifest).
|
|
161
|
+
* Missing manifest → defaults, silently (the returned `root` is still the project root —
|
|
162
|
+
* `dirname(manifestFilePath(cwd))`, the face's anchor). Unusable manifest (invalid shape /
|
|
163
|
+
* read error) → defaults + `console.warn` + a log event (visible degradation, never a throw).
|
|
164
|
+
* A retired `.thincoder/conventions.json` at the manifest's project root → one warning
|
|
165
|
+
* per cache miss (content zero-parsed, zero effect on the readings — KD-M1-34).
|
|
166
|
+
* @param {string} cwd — project dir / anchor (root resolved by manifest.mjs)
|
|
167
|
+
* @returns {Readonly<{declared: boolean, codePaths: readonly string[],
|
|
168
|
+
* index: {codeExtensions: string[], docExtensions: string[], excludePaths: string[], publicRepos: string[]},
|
|
169
|
+
* advisor: {docMap: string, standardsDoc: string}, root: string|null}>}
|
|
170
|
+
*/
|
|
171
|
+
export function loadProjectDeclaration(cwd) {
|
|
172
|
+
const file = manifestFilePath(cwd) // data-file path (single source — manifest.mjs KD-M1-18)
|
|
173
|
+
const hit = _cache.get(file)
|
|
174
|
+
if (hit) return hit
|
|
175
|
+
const root = dirname(file) // project root the manifest belongs to (cwd may be deeper) — the face's anchor
|
|
176
|
+
if (existsSync(join(root, RETIRED_REL_PATH))) warnRetiredCarrier(root)
|
|
177
|
+
let decl = buildDeclaration(null, root) // missing / unusable manifest → the defaults, root still known
|
|
178
|
+
try {
|
|
179
|
+
const r = readManifest(cwd)
|
|
180
|
+
if (r.ok) {
|
|
181
|
+
decl = buildDeclaration(r.manifest, root)
|
|
182
|
+
} else if (r.reason !== "missing") {
|
|
183
|
+
// Usable-file expectation broken (invalid JSON / wrong shape) → defaults, visible.
|
|
184
|
+
console.warn(`[declaration] ${file} is not a usable declaration (${(r.errors ?? []).join("; ") || r.reason}) — falling back to defaults`)
|
|
185
|
+
logEvent("declaration:error", { path: file, err: `invalid: ${(r.errors ?? []).join("; ")}`.slice(0, 200) })
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// Read error (EACCES …) — same visible degradation, never a throw.
|
|
189
|
+
console.warn(`[declaration] ${file} not readable (${e?.message ?? e}) — falling back to defaults`)
|
|
190
|
+
logEvent("declaration:error", { path: file, err: String(e?.message ?? e).slice(0, 200) })
|
|
191
|
+
}
|
|
192
|
+
_cache.set(file, decl)
|
|
193
|
+
return decl
|
|
194
|
+
}
|
package/embedding.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { RETRYABLE_STATUS } from "./provider/index.mjs"
|
|
9
|
+
import { resolveEnvRefs } from "./env-ref.mjs"
|
|
9
10
|
const MAX_RETRIES = 3
|
|
10
11
|
const BATCH_SIZE = 32 // max texts per request (within SiliconFlow limits)
|
|
11
12
|
|
|
@@ -16,7 +17,8 @@ export function createEmbedder(config) {
|
|
|
16
17
|
if (!config?.model) throw new Error("embedding config: model is required — configure embedding.model in ~/.thincoder/config.json")
|
|
17
18
|
return {
|
|
18
19
|
baseURL: config.baseURL.replace(/\/+$/, ""),
|
|
19
|
-
|
|
20
|
+
// #57(provider-config-family 批):值位 `${env:VAR}` 消费侧解析(CONFIG.md §6.3)——embedder 持解析值,请求侧零改。
|
|
21
|
+
apiKey: resolveEnvRefs(config.apiKey),
|
|
20
22
|
model: config.model,
|
|
21
23
|
}
|
|
22
24
|
}
|
package/env-ref.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env-ref.mjs — 值位 `${env:NAME}` 引用的消费侧解析(单源叶档;机制 = `docs/core/design/CONFIG.md` §6.3)。
|
|
3
|
+
*
|
|
4
|
+
* 语义(零 import 纯函数):
|
|
5
|
+
* - 整值与内嵌同解(`sk-${env:K}` 式);同一值可含多引用;非字符串值原样通过。
|
|
6
|
+
* - 变量未设 ∕ 空串 ⇒ 抛错(点名变量;无「未设即缺省」回退——不静默字面透传)。
|
|
7
|
+
* - 畸形引用(`${env:` 起头不构成合法引用)同抛错(点名畸形片段)。
|
|
8
|
+
* - 解析单遍:替换产物不再二次扫描;解析产物永不回显、永不回写(调用方纪律——写盘链按磁盘原文)。
|
|
9
|
+
*/
|
|
10
|
+
const REF_PREFIX = "${env:"
|
|
11
|
+
const REF_RE = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g
|
|
12
|
+
|
|
13
|
+
/** 读一个变量(未设 ∕ 空串 ⇒ 抛错,点名变量;错误句零含解析产物 ∕ 前置字面)。 */
|
|
14
|
+
function readVar(name) {
|
|
15
|
+
const v = process.env[name]
|
|
16
|
+
if (v === undefined || v === "") {
|
|
17
|
+
throw new Error(`env reference \${env:${name}}: variable "${name}" ${v === undefined ? "is not set" : "is empty"} — set the variable or replace the reference with a literal value`)
|
|
18
|
+
}
|
|
19
|
+
return v
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 畸形引用抛错(快照自 `${env:` 起截——引用串之前的字面不进错误句)。 */
|
|
23
|
+
function badRef(value, idx) {
|
|
24
|
+
return new Error(`malformed env reference ${JSON.stringify(value.slice(idx, idx + 24))} — expected \${env:NAME} with NAME matching [A-Za-z_][A-Za-z0-9_]*`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 字符串值解析:无引用 ⇒ 原串返回;逐引用替换(每次 `${env:` 出现必须构成合法引用)。 */
|
|
28
|
+
export function resolveEnvRefs(value) {
|
|
29
|
+
if (typeof value !== "string" || !value.includes(REF_PREFIX)) return value
|
|
30
|
+
const out = []
|
|
31
|
+
let pos = 0
|
|
32
|
+
while (pos < value.length) {
|
|
33
|
+
const idx = value.indexOf(REF_PREFIX, pos)
|
|
34
|
+
if (idx === -1) { out.push(value.slice(pos)); break }
|
|
35
|
+
REF_RE.lastIndex = idx
|
|
36
|
+
const m = REF_RE.exec(value)
|
|
37
|
+
if (!m || m.index !== idx) throw badRef(value, idx)
|
|
38
|
+
out.push(value.slice(pos, idx), readVar(m[1]))
|
|
39
|
+
pos = idx + m[0].length
|
|
40
|
+
}
|
|
41
|
+
return out.join("")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 键值对象解析(headers ∕ env 族):无引用 ⇒ 原对象返回(零拷贝);值非字符串原样。 */
|
|
45
|
+
export function resolveEnvRefMap(map) {
|
|
46
|
+
if (map === null || typeof map !== "object" || Array.isArray(map)) return map
|
|
47
|
+
let changed = false
|
|
48
|
+
const out = {}
|
|
49
|
+
for (const [k, v] of Object.entries(map)) {
|
|
50
|
+
const rv = resolveEnvRefs(v)
|
|
51
|
+
if (rv !== v) changed = true
|
|
52
|
+
out[k] = rv
|
|
53
|
+
}
|
|
54
|
+
return changed ? out : map
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** providers[] 条目适配:`apiKey` ∥ `headers.*`(无引用 ⇒ 原对象返回——零拷贝零写回)。 */
|
|
58
|
+
export function resolveProviderSecrets(provider) {
|
|
59
|
+
if (provider === null || typeof provider !== "object") return provider
|
|
60
|
+
const apiKey = resolveEnvRefs(provider.apiKey)
|
|
61
|
+
const headers = resolveEnvRefMap(provider.headers)
|
|
62
|
+
if (apiKey === provider.apiKey && headers === provider.headers) return provider
|
|
63
|
+
const out = { ...provider }
|
|
64
|
+
if (apiKey !== provider.apiKey) out.apiKey = apiKey
|
|
65
|
+
if (headers !== provider.headers) out.headers = headers
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** mcp.servers[] 条目适配:`token` ∥ `headers.*` ∥ `env.*`(无引用 ⇒ 原对象返回)。 */
|
|
70
|
+
export function resolveMcpServerSecrets(config) {
|
|
71
|
+
if (config === null || typeof config !== "object") return config
|
|
72
|
+
const token = resolveEnvRefs(config.token)
|
|
73
|
+
const headers = resolveEnvRefMap(config.headers)
|
|
74
|
+
const env = resolveEnvRefMap(config.env)
|
|
75
|
+
if (token === config.token && headers === config.headers && env === config.env) return config
|
|
76
|
+
const out = { ...config }
|
|
77
|
+
if (token !== config.token) out.token = token
|
|
78
|
+
if (headers !== config.headers) out.headers = headers
|
|
79
|
+
if (env !== config.env) out.env = env
|
|
80
|
+
return out
|
|
81
|
+
}
|
package/explore-distill.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* explore-distill.mjs — End-of-run exploration distillation (
|
|
3
|
-
* CONTEXT-COMPACTION §6.9 H1, 2026-08-23). 2026-09-05 module-split: moved verbatim out of
|
|
2
|
+
* explore-distill.mjs — End-of-run exploration distillation (CONTEXT-COMPACTION.md §6.9 H1, 2026-08-23). 2026-09-05 module-split: moved verbatim out of
|
|
4
3
|
* context.mjs (524 > 500 hard limit). The main agent's machine line is flooded by inline
|
|
5
4
|
* step-by-step exploration (read/grep/...). At run end we distill THIS run's exploration
|
|
6
5
|
* tool-results into one semantic summary note that replaces them in the machine line,
|
|
@@ -99,7 +98,7 @@ async function distillExplorations(history, start, provider, signal, agent, dept
|
|
|
99
98
|
messages: buildCompressMessages(history, cut, extras?.systemPrompt, EXPLORE_SUMMARY_PROMPT),
|
|
100
99
|
tools: extras?.tools,
|
|
101
100
|
signal,
|
|
102
|
-
// §
|
|
101
|
+
// TRACES.md §6.1 D-TR4:轨迹元数据增补——kind=distill(探索蒸馏面——agent 元数据透出;
|
|
103
102
|
// depth 经 summarizeRunExplorations 参数透传——agent.mjs 主作用域传入)
|
|
104
103
|
logCtx: {
|
|
105
104
|
stage: "distill", child: agent?._logId ?? null, kind: "distill",
|