@lmzhen/dsh-evolution-core 0.26.15 → 0.27.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 (116) hide show
  1. package/README.md +16 -1
  2. package/lib/composition/index.js +3 -0
  3. package/lib/composition-D8Y3-ROd.js +604 -0
  4. package/lib/ensure-MEYMj9jn.js +222 -0
  5. package/lib/errors-Br2-uUSK.js +57 -0
  6. package/lib/http-routes-BP-S3KV-.js +149 -0
  7. package/lib/http-routes.js +2 -0
  8. package/lib/index.js +777 -840
  9. package/lib/restrictor.js +37 -0
  10. package/lib/subagent/index.js +186 -0
  11. package/lib/types/citations.js +192 -0
  12. package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +2 -2
  13. package/lib/types/composition/composer.js +681 -0
  14. package/lib/types/composition/ensure.d.ts +10 -0
  15. package/lib/types/composition/ensure.js +95 -0
  16. package/lib/types/composition/index.d.ts +5 -0
  17. package/lib/types/composition/index.js +7 -0
  18. package/lib/types/composition/preset-ids.d.ts +17 -0
  19. package/lib/types/composition/preset-ids.js +21 -0
  20. package/lib/types/composition/restricted-preset.d.ts +36 -0
  21. package/lib/types/composition/restricted-preset.js +42 -0
  22. package/lib/types/constants.js +223 -0
  23. package/lib/types/cost.js +67 -0
  24. package/lib/types/curator.d.ts +2 -0
  25. package/lib/types/curator.js +346 -0
  26. package/lib/types/diagnostics.d.ts +26 -0
  27. package/lib/types/diagnostics.js +23 -0
  28. package/lib/types/document-body.js +91 -0
  29. package/lib/types/drift-signals.js +319 -0
  30. package/lib/types/env.js +25 -0
  31. package/lib/types/errors.d.ts +4 -1
  32. package/lib/types/errors.js +59 -0
  33. package/lib/types/events.js +16 -0
  34. package/lib/types/evidence.js +65 -0
  35. package/lib/types/evolution-events.js +479 -0
  36. package/lib/types/fold-untrusted.js +44 -0
  37. package/lib/types/frontmatter.js +623 -0
  38. package/lib/types/fuzzy-match.js +135 -0
  39. package/lib/types/gates.js +51 -0
  40. package/lib/types/guards.js +27 -0
  41. package/lib/types/health-baseline.js +63 -0
  42. package/lib/types/http-routes.d.ts +67 -35
  43. package/lib/types/http-routes.js +135 -0
  44. package/lib/types/index.d.ts +13 -2
  45. package/lib/types/index.js +101 -0
  46. package/lib/types/instance-scope.js +69 -0
  47. package/lib/types/io.js +675 -0
  48. package/lib/types/learn-prompt.js +42 -0
  49. package/lib/types/limits.js +63 -0
  50. package/lib/types/live.js +107 -0
  51. package/lib/types/long-runs.js +96 -0
  52. package/lib/types/memory-store.d.ts +0 -16
  53. package/lib/types/memory-store.js +606 -0
  54. package/lib/types/mutations.js +95 -0
  55. package/lib/types/namespace-migration.js +156 -0
  56. package/lib/types/numeric.js +47 -0
  57. package/lib/types/opt-in.js +101 -0
  58. package/lib/types/package-identity.d.ts +14 -0
  59. package/lib/types/package-identity.js +47 -0
  60. package/lib/types/params.js +328 -0
  61. package/lib/types/payload.js +36 -0
  62. package/lib/types/platform-line.generated.d.ts +10 -0
  63. package/lib/types/platform-line.generated.js +12 -0
  64. package/lib/types/platform-services.d.ts +2 -2
  65. package/lib/types/platform-services.js +52 -0
  66. package/lib/types/probe.d.ts +9 -3
  67. package/lib/types/probe.js +122 -0
  68. package/lib/types/prompts.js +409 -0
  69. package/lib/types/quality.js +308 -0
  70. package/lib/types/reachability.d.ts +46 -0
  71. package/lib/types/reachability.js +122 -0
  72. package/lib/types/redact.js +257 -0
  73. package/lib/types/reference-rewrite.js +144 -0
  74. package/lib/types/reports.js +110 -0
  75. package/lib/types/restrictor.d.ts +11 -0
  76. package/lib/types/restrictor.js +48 -0
  77. package/lib/types/review-channel.js +59 -0
  78. package/lib/types/run-registry.js +345 -0
  79. package/lib/types/scope.js +7 -0
  80. package/lib/types/serial.js +17 -0
  81. package/lib/types/session-fold.d.ts +15 -0
  82. package/lib/types/session-fold.js +28 -0
  83. package/lib/types/session-projection.js +295 -0
  84. package/lib/types/signals.js +209 -0
  85. package/lib/types/skill-discovery.js +66 -0
  86. package/lib/types/skill-health.js +134 -0
  87. package/lib/types/skill-history.js +745 -0
  88. package/lib/types/skill-lint.js +112 -0
  89. package/lib/types/skill-reads.js +67 -0
  90. package/lib/types/skill-row-cells.js +123 -0
  91. package/lib/types/skill-store.d.ts +34 -3
  92. package/lib/types/skill-store.js +3528 -0
  93. package/lib/types/state-store.js +60 -0
  94. package/lib/types/store-ledger.d.ts +44 -0
  95. package/lib/types/store-ledger.js +117 -0
  96. package/lib/types/subagent/channel.d.ts +4 -0
  97. package/lib/types/subagent/channel.js +131 -0
  98. package/lib/types/subagent/index.d.ts +4 -0
  99. package/lib/types/subagent/index.js +5 -0
  100. package/lib/types/subagent/readiness.d.ts +6 -0
  101. package/lib/types/subagent/readiness.js +35 -0
  102. package/lib/types/subagent/types.d.ts +58 -0
  103. package/lib/types/subagent/types.js +2 -0
  104. package/lib/types/threats.js +338 -0
  105. package/lib/types/tool-dispatch.d.ts +5 -0
  106. package/lib/types/tool-dispatch.js +535 -0
  107. package/lib/types/tree-write.d.ts +53 -0
  108. package/lib/types/tree-write.js +40 -0
  109. package/lib/types/unit-members.js +121 -0
  110. package/lib/types/usage.js +474 -0
  111. package/lib/types/write-authority.d.ts +87 -0
  112. package/lib/types/write-authority.js +23 -0
  113. package/lib/types/write-gates.d.ts +111 -0
  114. package/lib/types/write-gates.js +344 -0
  115. package/lib/types/write-inventory.js +158 -0
  116. package/package.json +39 -11
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Curator/author audit trail: `.mutations.json` records every skill mutation
3
+ * with before/after content hashes so any automated edit is reviewable and
4
+ * replayable. Best-effort persistence, mirroring the usage sidecar posture.
5
+ * @module @lmzhen/dsh-evolution-core
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { isRecord } from "./guards.js";
9
+ import { join } from 'node:path';
10
+ import { nodeEvolutionIo, transactIo } from "./io.js";
11
+ export const DEFAULT_MUTATION_CAP = 500;
12
+ /** Version of the `.mutations.json` file shape; writers always emit the current one. */
13
+ export const MUTATIONS_FILE_VERSION = 1;
14
+ export function mutationsFile(root) {
15
+ return join(root, '.mutations.json');
16
+ }
17
+ export function contentHash(content) {
18
+ // Content identity, not byte identity: Windows reads a file back with CRLF while the caller
19
+ // staged the in-memory LF, so hashing raw bytes reports `drift` for an unchanged skill.
20
+ return createHash('sha256').update(content.replaceAll('\r\n', '\n')).digest('hex');
21
+ }
22
+ /**
23
+ * Parse a raw mutations sidecar; malformed content reads as empty (auditing is
24
+ * best-effort). Versioned shape ({ version, records }) with legacy
25
+ * plain-array compat, plus a field-level guard for records without the
26
+ * required identity/timestamp fields (rc.42 audit P2-3).
27
+ */
28
+ function parseMutationRecords(raw) {
29
+ if (raw === null)
30
+ return [];
31
+ try {
32
+ return recordsFromParsed(JSON.parse(raw));
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ }
38
+ /** Field-level shape guard shared by the parse and the recordMutation write
39
+ * path (P3/v15: the guard's JSON.parse and this parse used to run twice over
40
+ * the same bytes inside one transact). */
41
+ function recordsFromParsed(parsed) {
42
+ const records = Array.isArray(parsed)
43
+ ? parsed
44
+ : typeof parsed === 'object' && parsed !== null && Array.isArray(parsed.records)
45
+ ? parsed.records
46
+ : [];
47
+ return records.filter((entry) => typeof entry === 'object' && entry !== null && typeof entry.skillName === 'string'
48
+ && typeof entry.action === 'string'
49
+ && typeof entry.at === 'string');
50
+ }
51
+ export async function loadMutations(root, io = nodeEvolutionIo()) {
52
+ return parseMutationRecords(await io.readText(mutationsFile(root)));
53
+ }
54
+ /** Append one record, trim to `cap`, and write atomically (versioned shape). */
55
+ export async function recordMutation(root, io, record, cap = DEFAULT_MUTATION_CAP) {
56
+ // The read-append-write runs as one atomic transact (rc.50 P2-2): a second
57
+ // process sharing DSH_HOME can no longer interleave its append between our
58
+ // read and write and lose an audit record.
59
+ await transactIo(io, mutationsFile(root), (current) => {
60
+ // P3 (v3 audit): never overwrite a malformed audit file with a re-serialized empty.
61
+ // P3 (v15): parse exactly ONCE — the malformed guard and the record
62
+ // extraction used to JSON.parse the same bytes twice inside one transact
63
+ // (the guard distinguishes malformed-JSON-preserve from
64
+ // valid-but-shapeless-migrate, which is why the two checks are separate
65
+ // even though the parse is shared).
66
+ let parsed = [];
67
+ if (current !== null) {
68
+ try {
69
+ parsed = JSON.parse(current);
70
+ }
71
+ catch {
72
+ // C-08: the drop is not silent — every append against the malformed
73
+ // file warns (the wording used to claim "warn once"; the guard has no
74
+ // once-guard and each dropped append is independently worth seeing).
75
+ // Still best-effort — no retry, and the mutation itself never fails
76
+ // on auditing.
77
+ console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
78
+ return current;
79
+ }
80
+ }
81
+ // A2-11 (v18): a newer on-disk version is never downgraded by this writer.
82
+ if (isRecord(parsed)) {
83
+ const version = parsed.version;
84
+ if (typeof version === 'number' && version > MUTATIONS_FILE_VERSION) {
85
+ console.warn(`mutation audit record dropped: ${mutationsFile(root)} declares version ${version} (newer than ${MUTATIONS_FILE_VERSION}); not overwritten`);
86
+ return current;
87
+ }
88
+ }
89
+ const existing = recordsFromParsed(parsed);
90
+ existing.push(record);
91
+ const trimmed = existing.length > cap ? existing.slice(existing.length - cap) : existing;
92
+ return JSON.stringify({ version: MUTATIONS_FILE_VERSION, records: trimmed }, null, 2);
93
+ });
94
+ }
95
+ //# sourceMappingURL=mutations.js.map
@@ -0,0 +1,156 @@
1
+ /**
2
+ * G3: the one-time move of the family's LEGACY settings sections into the row ids the
3
+ * platform keys settings by now.
4
+ *
5
+ * On 0.1.x each plugin registered a free-form namespace (`evolution-memory`, …) through the
6
+ * family's own seam. The platform derives a settings namespace from the Loader entry id
7
+ * today, so those old strings address nothing: the platform's own legacy import finds no
8
+ * such entry, warns, and leaves the values in the renamed document. This module reads that
9
+ * document, maps each legacy section (and each deprecated key spelling) onto its row, and
10
+ * merges the values into that row's user layer.
11
+ *
12
+ * Two halves, both testable without a host: {@link planNamespaceMigration} is pure (text in,
13
+ * plan out) and {@link applyNamespaceMigration} takes the few things it needs as
14
+ * dependencies, so the side-effecting half runs against a fake settings seat.
15
+ * @module @lmzhen/dsh-evolution-core
16
+ */
17
+ import { parse as parseYaml } from 'yaml';
18
+ import { isRecord as isSection } from "./guards.js";
19
+ import { NAMESPACE_MIGRATIONS, PARAM_ALIASES, PARAM_EXPOSURE, resolveParamId } from "./params.js";
20
+ /**
21
+ * The key names one row may be written with: its own E3 rows, plus the deprecated
22
+ * spellings that still resolve to them. An unknown key is refused instead of written —
23
+ * the platform validates a write against the row's schema, so writing it would fail the
24
+ * whole row's migration.
25
+ * @param rowId - the row's entry id.
26
+ * @returns every key name a legacy section may carry for that row.
27
+ */
28
+ function writableKeys(rowId) {
29
+ const keys = new Set();
30
+ for (const entry of PARAM_EXPOSURE) {
31
+ if (entry.owner === rowId && entry.tier === 'E3')
32
+ keys.add(entry.id);
33
+ }
34
+ for (const [alias, canonical] of Object.entries(PARAM_ALIASES)) {
35
+ if (keys.has(canonical))
36
+ keys.add(alias);
37
+ }
38
+ return keys;
39
+ }
40
+ /**
41
+ * Read one legacy settings document into a migration plan.
42
+ *
43
+ * Pure: the caller reads the file (the platform's renamed `settings.yaml.imported` first,
44
+ * then a `settings.yaml` the platform has not imported yet) and hands the text over.
45
+ * @param documentText - the YAML document text.
46
+ * @returns what to write, what the family knows but need not touch, and what it refused.
47
+ * @throws {Error} when the document is not a mapping, or a family section is not one.
48
+ */
49
+ export function planNamespaceMigration(documentText) {
50
+ const parsed = parseYaml(documentText);
51
+ if (parsed === undefined || parsed === null)
52
+ return { entries: [], empty: [], foreign: [], skipped: [] };
53
+ if (!isSection(parsed))
54
+ throw new Error('the settings document is not a mapping; refusing to migrate it');
55
+ const legacy = new Map(NAMESPACE_MIGRATIONS.map(entry => [entry.namespace, entry.rowId]));
56
+ const entries = [];
57
+ const empty = [];
58
+ const skipped = [];
59
+ for (const [namespace, section] of Object.entries(parsed)) {
60
+ const rowId = legacy.get(namespace);
61
+ if (rowId === undefined)
62
+ continue;
63
+ if (!isSection(section))
64
+ throw new Error('settings section `' + namespace + '` is not a mapping; refusing to migrate it');
65
+ const allowed = writableKeys(rowId);
66
+ const values = [];
67
+ for (const [key, value] of Object.entries(section)) {
68
+ const canonical = resolveParamId(key);
69
+ if (!allowed.has(canonical)) {
70
+ skipped.push({ namespace, key, reason: 'row `' + rowId + '` 没有这个参数' });
71
+ continue;
72
+ }
73
+ values.push({ source: key, key: canonical, value });
74
+ }
75
+ if (values.length === 0)
76
+ empty.push(namespace);
77
+ else
78
+ entries.push({ namespace, rowId, values });
79
+ }
80
+ const foreign = Object.keys(parsed).filter(name => !legacy.has(name)).sort();
81
+ return { entries, empty, foreign, skipped };
82
+ }
83
+ /**
84
+ * Whether two JSON-shaped config values are the same value.
85
+ * @param left - one value.
86
+ * @param right - the other.
87
+ * @returns true when both serialize identically.
88
+ */
89
+ function sameValue(left, right) {
90
+ return JSON.stringify(left) === JSON.stringify(right);
91
+ }
92
+ /**
93
+ * Decide what a plan would do: the rows it would write, the keys already equal, the stored
94
+ * values that win, and the refusals. The write half and the read-only doctor read the SAME
95
+ * decision here — one rule, two consumers, so a report can never disagree with a run.
96
+ * @param plan - the plan from {@link planNamespaceMigration}.
97
+ * @param read - the row's current user layer (see {@link NamespaceMigrationDeps.read}).
98
+ * @returns the decision; nothing is written.
99
+ */
100
+ export function classifyNamespaceMigration(plan, read) {
101
+ const write = [];
102
+ const conflicts = [];
103
+ const unchanged = [];
104
+ const skipped = [...plan.skipped];
105
+ for (const entry of plan.entries) {
106
+ // A row this composition does not serve (the `variant` install form keeps the model rows
107
+ // in an agent preset, so they are mounted per session, not here) has no section to write:
108
+ // reporting it keeps ONE unmounted row from aborting the rows that ARE mounted.
109
+ const view = read(entry.rowId);
110
+ if (view === undefined) {
111
+ for (const value of entry.values) {
112
+ skipped.push({ namespace: entry.namespace, key: value.source, reason: 'row `' + entry.rowId + '` 在本组合中不提供服务' });
113
+ }
114
+ continue;
115
+ }
116
+ const user = view.user ?? {};
117
+ const patch = {};
118
+ for (const value of entry.values) {
119
+ if (!Object.hasOwn(user, value.key)) {
120
+ patch[value.key] = value.value;
121
+ continue;
122
+ }
123
+ if (sameValue(user[value.key], value.value)) {
124
+ unchanged.push({ rowId: entry.rowId, key: value.key });
125
+ continue;
126
+ }
127
+ conflicts.push({ rowId: entry.rowId, key: value.key, current: user[value.key], legacy: value.value });
128
+ }
129
+ if (Object.keys(patch).length > 0)
130
+ write.push({ rowId: entry.rowId, patch });
131
+ }
132
+ return { write, conflicts, unchanged, skipped };
133
+ }
134
+ /**
135
+ * Report one kept value: the stored value is the newer decision, so it wins and the legacy
136
+ * one is named rather than dropped.
137
+ * @param conflict - the conflict the classification found.
138
+ * @returns the warning text.
139
+ */
140
+ function conflictWarning(conflict) {
141
+ return 'evolution-migration: row `' + conflict.rowId + '` already sets `' + conflict.key +
142
+ '`; the stored value was kept and the legacy ' + JSON.stringify(conflict.legacy) + ' was not applied';
143
+ }
144
+ export async function applyNamespaceMigration(plan, deps) {
145
+ // An arrow keeps the dependency's own receiver contract intact (the platform's read binds a seat).
146
+ const decided = classifyNamespaceMigration(plan, rowId => deps.read(rowId));
147
+ for (const conflict of decided.conflicts)
148
+ deps.warn(conflictWarning(conflict));
149
+ const written = [];
150
+ for (const row of decided.write) {
151
+ await deps.update(row.rowId, row.patch);
152
+ written.push({ rowId: row.rowId, keys: Object.keys(row.patch) });
153
+ }
154
+ return { written, conflicts: decided.conflicts, unchanged: decided.unchanged, skipped: decided.skipped };
155
+ }
156
+ //# sourceMappingURL=namespace-migration.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Numeric config clamping for the dsh-evolution plugin family.
3
+ *
4
+ * G3.1 (0.3.23): numeric configuration values are normalised through a single
5
+ * helper so 0 / negative / NaN / ±Infinity / out-of-range all fall back to the
6
+ * package default instead of silently becoming a "disabled" special value or
7
+ * folding as NaN into a computation. A non-finite value (NaN, ±Infinity) is
8
+ * never a legitimate config, so it always falls back. The schema layer (a
9
+ * `.min(1)` clamp on the number schema) is a first-line guard where it can
10
+ * reject invalid values; this helper is the mandatory assembly-time clamp that
11
+ * also catches what schemastery lets through (NaN and +Infinity both pass a
12
+ * bare number schema).
13
+ *
14
+ * The scope here is pure numeric conversion only (no schemastery import), so
15
+ * evolution-core stays free of a schemastery dependency. Per-package Config
16
+ * schemas keep their own `.min()`/`.default()` call sites.
17
+ * @module @lmzhen/dsh-evolution-core
18
+ */
19
+ /**
20
+ * Clamp a numeric config to the `[min, max]` range.
21
+ *
22
+ * A non-finite value (NaN, ±Infinity), a non-number, or a value outside the
23
+ * inclusive range falls back to `fallback`. When `opts.min >= 1`, 0 and
24
+ * negative values also fall back to `fallback` — a 0 is never treated as a
25
+ * special "disabled" meaning (G3.1 decision). Callers that legitimately allow
26
+ * 0 (e.g. a threshold or a zero-cost weight) pass `{ min: 0 }` or omit the
27
+ * range.
28
+ *
29
+ * @param value - the raw numeric config, possibly `undefined` (absent).
30
+ * @param fallback - the default value to return when the value is invalid.
31
+ * @param opts - optional inclusive lower/upper bound.
32
+ * @returns `value` when it is a finite number within range, else `fallback`.
33
+ */
34
+ export function clampedNumber(value, fallback, opts) {
35
+ if (typeof value !== 'number' || !Number.isFinite(value))
36
+ return fallback;
37
+ const { min, max } = opts ?? {};
38
+ if (min !== undefined && value < min)
39
+ return fallback;
40
+ if (max !== undefined && value > max)
41
+ return fallback;
42
+ return value;
43
+ }
44
+ // The clamp-AND-WARN helper (`clampOnce`) and the live/plain unwrap now live in
45
+ // `live.ts` — one reader for all config shapes, still exported from the package barrel,
46
+ // so importers keep working unchanged. This module owns the pure numeric conversion only.
47
+ //# sourceMappingURL=numeric.js.map
@@ -0,0 +1,101 @@
1
+ import { scopeOf } from '@deepseek-ai/dsh-scope';
2
+ /**
3
+ * Tool names only a session that mounted the family's MODEL rows can see.
4
+ *
5
+ * Both are family packages (`tool-skill-manage`, `tool-memory`), so a session
6
+ * that merely carries the platform's own session-query tool is not mistaken for
7
+ * a family session. Two names, not one: a deployment may disable either row.
8
+ */
9
+ export const FAMILY_SESSION_TOOL_NAMES = ['skill_manage', 'memory'];
10
+ /**
11
+ * S0-4 (v43 J-1 / G-1): the process-wide witness behind the deployment
12
+ * diagnostic. A scoped false is the CORRECT answer for a session that did not
13
+ * opt in, so the miss alone proves nothing — "no session in this process ever
14
+ * matched" is the shape both real faults share, and only a witness that outlives
15
+ * one session can tell it from a healthy per-session skip.
16
+ *
17
+ * Module scope on purpose: the claim spans every session and every row of this
18
+ * process, so no single row's fiber owns it. Monotone scalars with no per-key
19
+ * lifecycle — the N12 registry covers module-scope Set/Map/WeakMap stores, which
20
+ * carry entries that do need one.
21
+ */
22
+ let scopedProbeHits = 0;
23
+ let scopedProbeMisses = 0;
24
+ let scopedProbeWarned = false;
25
+ /**
26
+ * Read the witness for a diagnostic surface (`/evolution doctor`). Read-only: it
27
+ * neither evaluates the probe nor consumes the one-time warn, so a report run
28
+ * cannot change what the next miss would have logged.
29
+ * @returns the verdict with both counts, zeroed in a process where the gate has
30
+ * not run.
31
+ */
32
+ export function scopedProbeReport() {
33
+ const verdict = scopedProbeHits > 0 ? 'hit' : scopedProbeMisses > 0 ? 'never-hit' : 'idle';
34
+ return { verdict, hits: scopedProbeHits, misses: scopedProbeMisses };
35
+ }
36
+ /**
37
+ * Does this session's scope see the family's model tools?
38
+ *
39
+ * The scope is the live agent's — the platform's own addressing for "what does
40
+ * this session see" (`tools.get(name, scope)`, packages/core/tools/src/index.ts:1194).
41
+ * A session with no live agent (a finished or cold session) has no scope to ask
42
+ * in, so it resolves to false; the consumers this gate serves only run for live
43
+ * agents.
44
+ * @param ctx - a context of the runtime (any plane; the services are read from the global store).
45
+ * @param sessionId - the session to ask about.
46
+ * @returns true when at least one family model tool is visible to that session.
47
+ */
48
+ export function sessionSeesFamilyTools(ctx, sessionId) {
49
+ const probe = ctx;
50
+ const tools = probe.get('tools');
51
+ if (tools === undefined)
52
+ return false;
53
+ const agent = probe.get('agents')?.get(sessionId);
54
+ const scope = agent?.ctx === undefined ? undefined : scopeOf(agent.ctx);
55
+ return FAMILY_SESSION_TOOL_NAMES.some(name => tools.get(name, scope) !== undefined);
56
+ }
57
+ /**
58
+ * S0-4 (v43 J-1 / G-1): record a scoped rejection and leave the one-time
59
+ * deployment diagnostic.
60
+ *
61
+ * Fires at most once per process, on the first miss, and only while nothing has
62
+ * ever matched. The message names the check rather than guessing the deployment
63
+ * form: a host-only install and a disabled model row are the two shapes that can
64
+ * never match, while the layered variant form legitimately waits for a session
65
+ * that selected the Evolution preset.
66
+ * @param ctx - the asking row's context; the warn rides that row's own logger.
67
+ */
68
+ function noteScopedProbeMiss(ctx) {
69
+ scopedProbeMisses += 1;
70
+ if (scopedProbeHits > 0 || scopedProbeWarned)
71
+ return;
72
+ scopedProbeWarned = true;
73
+ ctx.logger.warn('dsh-evolution: sessionScoped is on but the family-tool probe has never matched a session in this process'
74
+ + ' — review injection and skill-usage telemetry are inert for every session seen so far.'
75
+ + ' Check whether the family\'s model rows are mounted at all: tool-memory / tool-skill-manage are devDependencies of'
76
+ + ' @lmzhen/dsh-evolution-host, so a HOST-ONLY install can never match, and a profile overlay that disables either row'
77
+ + ' has the same effect. Under the layered VARIANT form only a session on the Evolution preset matches, so a miss there'
78
+ + ' just means none has run yet. Next: /evolution doctor reports the scoped rows and this verdict (see INSTALL.md).');
79
+ }
80
+ /**
81
+ * The one decision every cross-session consumer calls before it acts.
82
+ * @param ctx - a context of the runtime.
83
+ * @param sessionId - the session the event belongs to.
84
+ * @param sessionScoped - the row's `sessionScoped` config.
85
+ * @returns true when the consumer may act on this session. A deployment that did
86
+ * not declare session scoping always answers true (the historical behavior);
87
+ * a scoped one answers true only for a session that carries the family's model
88
+ * tools. Each scoped answer updates the process witness, and the first miss with
89
+ * no match ever leaves one warn (see {@link noteScopedProbeMiss}).
90
+ */
91
+ export function sessionAudited(ctx, sessionId, sessionScoped) {
92
+ if (sessionScoped !== true)
93
+ return true;
94
+ if (sessionSeesFamilyTools(ctx, sessionId)) {
95
+ scopedProbeHits += 1;
96
+ return true;
97
+ }
98
+ noteScopedProbeMiss(ctx);
99
+ return false;
100
+ }
101
+ //# sourceMappingURL=opt-in.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The scope segment of the nearest `package.json` at or above `from`.
3
+ * @param from - the calling module URL (its own manifest is the answer).
4
+ * @returns the scope, e.g. `@deepseek-ai` or `@lmzhen`; an unscoped manifest yields an empty string.
5
+ */
6
+ export declare function scopeOfNearestManifest(from: string | URL): string;
7
+ /**
8
+ * One family/platform package name under the RUNNING scope.
9
+ * @param bareName - the name without any scope, e.g. `dsh-agent-preset`.
10
+ * @param from - the calling module URL.
11
+ * @returns `scope/bareName`, or `bareName` when the manifest carries no scope.
12
+ */
13
+ export declare function scopedPackageName(bareName: string, from: string | URL): string;
14
+ //# sourceMappingURL=package-identity.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * G1/1.8 (G-04): the SCOPE this build was published under, DERIVED from a manifest instead of written as a
3
+ * literal in running code.
4
+ *
5
+ * The family used to hard-code `@deepseek-ai/dsh-...` names and let `prepare-release.mjs --scope @lmzhen`
6
+ * patch the text afterwards. A published install therefore depended on a release-time string rewrite
7
+ * reaching every copy, and the copies had already drifted (doctor.ts, index.ts, preset-composition.ts and
8
+ * its .mjs mirror each spelled the identity). The scope is knowable at runtime: it is the scope segment of
9
+ * the nearest `package.json` above the calling module. Nothing to rewrite, one owner of the answer.
10
+ *
11
+ * @module @lmzhen/dsh-evolution-core/package-identity
12
+ */
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ /**
17
+ * The scope segment of the nearest `package.json` at or above `from`.
18
+ * @param from - the calling module URL (its own manifest is the answer).
19
+ * @returns the scope, e.g. `@deepseek-ai` or `@lmzhen`; an unscoped manifest yields an empty string.
20
+ */
21
+ export function scopeOfNearestManifest(from) {
22
+ let dir = dirname(from instanceof URL ? fileURLToPath(from) : fileURLToPath(new URL(from)));
23
+ for (let hops = 0; hops < 32; hops += 1) {
24
+ const manifest = join(dir, 'package.json');
25
+ if (existsSync(manifest)) {
26
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
27
+ const name = typeof parsed.name === 'string' ? parsed.name : '';
28
+ return name.startsWith('@') ? (name.split('/')[0] ?? '') : '';
29
+ }
30
+ const parent = dirname(dir);
31
+ if (parent === dir)
32
+ break;
33
+ dir = parent;
34
+ }
35
+ return '';
36
+ }
37
+ /**
38
+ * One family/platform package name under the RUNNING scope.
39
+ * @param bareName - the name without any scope, e.g. `dsh-agent-preset`.
40
+ * @param from - the calling module URL.
41
+ * @returns `scope/bareName`, or `bareName` when the manifest carries no scope.
42
+ */
43
+ export function scopedPackageName(bareName, from) {
44
+ const scope = scopeOfNearestManifest(from);
45
+ return scope === '' ? bareName : scope + '/' + bareName;
46
+ }
47
+ //# sourceMappingURL=package-identity.js.map