@lmzhen/dsh-evolution-core 0.16.1 → 0.17.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.
package/lib/index.js CHANGED
@@ -3,9 +3,10 @@ import { basename, dirname, join, resolve } from "node:path";
3
3
  import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
5
  import { homedir } from "node:os";
6
- import { readFileSync } from "node:fs";
6
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { scopeOf } from "@deepseek-ai/dsh-scope";
9
+ import { z } from "zod";
9
10
  //#region lib/types/citations.js
10
11
  /**
11
12
  * Citation resolution for skill bodies and support files (design §5.2).
@@ -3220,6 +3221,8 @@ const EVOLUTION_ERRORS = Object.freeze({
3220
3221
  "e-305-this-invocation-carries-no-2": "E-305: this invocation carries no agent — `%a1%` needs a session-backed call (run it from a session in the GUI or the CLI).",
3221
3222
  "e-317-skill-write-not-confirmed": "E-317: skill \"%a1%\" was not %a2% — the confirmation prompt was declined or dismissed, so nothing was written. The prompt appears on every create and bare delete; repeat the call only if the operator asks for it.",
3222
3223
  "e-318-skill-write-without-a-read": "E-318: skill \"%a1%\" was not read in this session, so this write is refused. Read it with the `skill` tool first (a read that failed does not count), then repeat the write.",
3224
+ "e-320-the-profile-home-is-not": "E-320: the profile home is not reported by this host, so the legacy settings document cannot be located. /evolution migrate reads `<profile home>/settings.yaml.imported`; run it from a profile-launched session.",
3225
+ "e-321-no-io-provider-is": "E-321: no evolution-io provider is mounted, so the legacy settings document cannot be read. Mount the evolution-io row (evolution-host/evolution-all) and retry.",
3223
3226
  "e-319-skill-write-confirm-timed-out": "E-319: skill \"%a1%\" was not %a2% — the confirmation prompt went unanswered for %a3% second(s), so the write was cancelled. Nobody was there to decide: set `skillWriteConfirm` to `auto` to write without a prompt, keep `ask` to wait until someone answers, or raise `skillWriteConfirmTimeoutSeconds`."
3224
3227
  });
3225
3228
  /**
@@ -4573,6 +4576,426 @@ function compositionRowIds(composition) {
4573
4576
  }
4574
4577
  return ids;
4575
4578
  }
4579
+ /** The platform package a DECLARATIVE preset row names (0.2.x replaced preset directories with it). */
4580
+ const AGENT_PRESET_PACKAGE = "@deepseek-ai/dsh-agent-preset";
4581
+ /**
4582
+ * Compose the DECLARATIVE preset row from the platform base composition and the family delta.
4583
+ *
4584
+ * 0.2.x removed the directory mechanism: `dsh-agent-preset` is an ordinary composition row whose
4585
+ * `config.plugins` carries the preset's complete child plugin list
4586
+ * (`packages/preset/agent-preset/src/index.ts`, `agent-preset-registry/src/definition.ts` — the
4587
+ * definition has NO base/extends field, which is why the platform base is merged in here).
4588
+ *
4589
+ * The composition itself is {@link composePresetComposition}: same row-collision contract and the
4590
+ * same `row-overrides.json` injection, applied while the composed rows are still at column 0 — the
4591
+ * override anchors are defined against a column-0 `- id:` row, so the indentation that puts them
4592
+ * under `plugins:` must come last.
4593
+ * @param baseComposition - the platform base preset patch/composition rows.
4594
+ * @param deltaComposition - the family delta rows.
4595
+ * @param identity - row id, preset id, display fields and order.
4596
+ * @returns the row text, newline-terminated, ready to merge into a profile patch.
4597
+ */
4598
+ /**
4599
+ * Drop the blank lines that merely separate block-sequence items, keeping the ones INSIDE a block
4600
+ * literal (`key: |` / `>`): there a blank line is part of the value, and filtering it flattened a
4601
+ * platform base preset's multi-paragraph prompt into one paragraph (G6 review, P1).
4602
+ * @param text - the composition rows.
4603
+ * @returns the rows with the separating blank lines removed.
4604
+ */
4605
+ function denseBlockSequence(text) {
4606
+ const out = [];
4607
+ let scalarIndent = -1;
4608
+ for (const line of text.split("\n")) {
4609
+ const trimmed = line.trim();
4610
+ const indent = line.length - line.trimStart().length;
4611
+ if (scalarIndent >= 0 && trimmed !== "" && indent <= scalarIndent) scalarIndent = -1;
4612
+ if (scalarIndent < 0) {
4613
+ const match = /^(\s*)[\w".-]+:\s*[|>]/.exec(line);
4614
+ if (match !== null) scalarIndent = (match[1] ?? "").length;
4615
+ }
4616
+ if (trimmed === "" && scalarIndent < 0) continue;
4617
+ out.push(line);
4618
+ }
4619
+ return out.join("\n");
4620
+ }
4621
+ function composePresetRow(baseComposition, deltaComposition, identity) {
4622
+ const plugins = denseBlockSequence(composePresetComposition(baseComposition, deltaComposition)).split("\n").map((line) => line === "" ? "" : " " + line).join("\n");
4623
+ return [
4624
+ "- id: " + identity.rowId,
4625
+ " name: '@deepseek-ai/dsh-agent-preset'",
4626
+ " config:",
4627
+ " id: " + identity.id,
4628
+ " name: " + JSON.stringify(identity.name),
4629
+ " description: " + JSON.stringify(identity.description),
4630
+ " order: " + String(identity.order),
4631
+ " plugins:",
4632
+ plugins,
4633
+ ""
4634
+ ].join("\n");
4635
+ }
4636
+ /** The patch-entry key that ADDS a row: a plain entry overrides the row with the same id, so a
4637
+ * preset the platform does not ship has to arrive inside an `insert` entry
4638
+ * (`packages/boot/app-boot/tests/user-patches.spec.ts:47-64`). */
4639
+ const PATCH_INSERT_KEY = "insert";
4640
+ /**
4641
+ * Wrap a composed row as the patch entry a profile patch needs.
4642
+ *
4643
+ * The row {@link composePresetRow} returns starts at column 0; a patch entry's list nests it four
4644
+ * spaces deeper. Wrapping is a separate step because the same row text is also what
4645
+ * {@link mergePresetRow} matches by id, and because the composer's override anchors are defined
4646
+ * against column-0 rows.
4647
+ * @param row - the row text, as {@link composePresetRow} returns it.
4648
+ * @returns the patch entry text, newline-terminated.
4649
+ */
4650
+ function composePresetInsert(row) {
4651
+ return `- ${PATCH_INSERT_KEY}:\n${row.replace(/\s+$/, "").split("\n").map((line) => " " + line).join("\n")}\n`;
4652
+ }
4653
+ /**
4654
+ * The row list a platform base preset patch carries under `config.plugins`, dedented to column 0.
4655
+ *
4656
+ * A base preset arrives as a whole patch entry — `- insert:` / `- id: preset-standard` /
4657
+ * `config:` / `plugins:` — because that is how the platform ships it
4658
+ * (`packages/bundle/web-app/presets/standard.patch.yml`). The preset's own list is the
4659
+ * SHALLOWEST `plugins:` line: a child row may carry a key by that name far deeper.
4660
+ * @param patchText - the base preset patch file's text.
4661
+ * @returns the plugin rows, dedented and newline-terminated.
4662
+ * @throws {Error} when the patch carries no `plugins:` list.
4663
+ */
4664
+ function basePresetPlugins(patchText) {
4665
+ const lines = patchText.split("\n");
4666
+ let at = -1;
4667
+ let indent = Number.POSITIVE_INFINITY;
4668
+ for (const [index, line] of lines.entries()) {
4669
+ const width = /^(\s*)plugins:\s*$/.exec(line)?.[1]?.length;
4670
+ if (width === void 0) continue;
4671
+ if (width < indent) {
4672
+ indent = width;
4673
+ at = index;
4674
+ }
4675
+ }
4676
+ if (at < 0) throw new Error("evolution preset composition: the base preset patch carries no `plugins:` list");
4677
+ const block = [];
4678
+ for (const line of lines.slice(at + 1)) {
4679
+ if (line.trim() === "") {
4680
+ block.push(line);
4681
+ continue;
4682
+ }
4683
+ if (line.length - line.trimStart().length <= indent) break;
4684
+ block.push(line);
4685
+ }
4686
+ while (block.length > 0 && block[block.length - 1]?.trim() === "") block.pop();
4687
+ const widths = block.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
4688
+ const dedent = widths.length === 0 ? 0 : Math.min(...widths);
4689
+ return block.map((line) => line.slice(0, dedent).trim() === "" ? line.slice(dedent) : line).join("\n") + "\n";
4690
+ }
4691
+ /** The row id a family preset occupies: the platform ships \`preset-standard\` for id `standard`
4692
+ * (`packages/bundle/web-app/presets/standard.patch.yml`), so a family preset id mirrors it. */
4693
+ function presetRowId(id) {
4694
+ return `preset-${id}`;
4695
+ }
4696
+ /**
4697
+ * The patch entry a profile patch receives for one preset: base rows plus the family delta, wrapped
4698
+ * as \`- insert:\`.
4699
+ *
4700
+ * The two halves are also exposed on their own — {@link composePresetRow} for the row, so
4701
+ * {@link mergePresetRow} can match it by id, and {@link basePresetPlugins} to read the platform
4702
+ * base out of a shipped patch.
4703
+ * @param basePatchText - the platform base preset patch's text.
4704
+ * @param deltaComposition - the family delta rows.
4705
+ * @param identity - row id, preset id, display fields and order.
4706
+ * @returns the patch entry text, newline-terminated.
4707
+ */
4708
+ function composePresetEntry(basePatchText, deltaComposition, identity) {
4709
+ return composePresetInsert(composePresetRow(basePresetPlugins(basePatchText), deltaComposition, identity));
4710
+ }
4711
+ /**
4712
+ * The text of one row's own patch item, or null when the patch carries no such row.
4713
+ *
4714
+ * The comparison unit for a freshness check: this is byte-for-byte what {@link composePresetEntry}
4715
+ * writes for the same row id, whether the row sits inside an \`- insert:\` entry (installer-written)
4716
+ * or at column 0 (saved by the Web editor).
4717
+ * @param patchText - the patch text.
4718
+ * @param rowId - the row id to read.
4719
+ * @returns the item's text, newline-terminated, or null.
4720
+ */
4721
+ function presetRowBody(block) {
4722
+ const lines = block.split("\n");
4723
+ if (!/^- insert:\s*$/.test(lines[0] ?? "")) return block;
4724
+ return lines.slice(1).map((line) => line.startsWith(" ") ? line.slice(4) : line).join("\n");
4725
+ }
4726
+ /**
4727
+ * The row BODY of a patch block, whichever form it is in.
4728
+ *
4729
+ * {@link presetRowBlock} returns the enclosing item verbatim: an installer-written row sits inside
4730
+ * `- insert:` while a row the platform's Web editor saved IS that column-0 item. A comparison must
4731
+ * compare like with like — comparing the raw block against `composePresetEntry()` (which always
4732
+ * starts with `- insert:`) reported DIFFERS for every editor-saved row, forever (G6 review, P2), and
4733
+ * the remedy it printed (`re-run the installer`) was refused by the `exists; use --force` guard.
4734
+ * @param block - a block as {@link presetRowBlock} returns it.
4735
+ * @returns the row text, `- insert:` unwrapped and its four-space indent stripped.
4736
+ */
4737
+ function presetRowBlock(patchText, rowId) {
4738
+ const lines = patchText.split("\n");
4739
+ const span = rowBlockSpan(lines, rowId);
4740
+ if (span === null) return null;
4741
+ const block = [...lines.slice(span.start, span.end)];
4742
+ while (block.length > 0 && (block[block.length - 1] ?? "").trim() === "") block.pop();
4743
+ return block.join("\n") + "\n";
4744
+ }
4745
+ /** The package every shipped base preset patch declares its row with
4746
+ * (`packages/bundle/web-app/presets/standard.patch.yml`). The platform's only bundle carrying
4747
+ * preset patches today; a bundle a caller mounts is probed beside it. */
4748
+ const PLATFORM_BUNDLE_PACKAGE = "@deepseek-ai/dsh-web-app";
4749
+ /**
4750
+ * What `DSH_AGENT_PRESET_ROOT` must name: a directory holding the platform base preset patches
4751
+ * under their shipped file names (`<base>.patch.yml`) — e.g. a copy of a same-version platform
4752
+ * tree's `packages/bundle/web-app/presets/`. The desktop application is the shape that needs it:
4753
+ * its platform packages live inside `resources/app.asar`, so there is no plain
4754
+ * `node_modules/@deepseek-ai/dsh-web-app/presets/` on disk for a source installer to read.
4755
+ */
4756
+ const AGENT_PRESET_ROOT_HINT = "point DSH_AGENT_PRESET_ROOT at a directory holding <base>.patch.yml (copy it out of a same-version dsh tree: packages/bundle/web-app/presets/)";
4757
+ /**
4758
+ * Resolve the platform patch file one `--base` names.
4759
+ *
4760
+ * 0.2.x ships each base preset as a bundle patch layer
4761
+ * (`packages/bundle/web-app/presets/<base>.patch.yml`, declared in that package's
4762
+ * `dsh.bundle.patch`), so the FILE — not a preset directory — is the base a family row is composed
4763
+ * from. Candidates, in order: an explicit `DSH_AGENT_PRESET_ROOT`; a module-graph probe when the
4764
+ * caller supplies one (a desktop/Electron host resolves the bundle inside `app.asar`, where no file
4765
+ * walk can reach); the target profile's own `node_modules`; then, walking up from the caller,
4766
+ * every `presets/` directory of a checkout's bundle packages, then every
4767
+ * `@deepseek-ai/dsh-<name>/presets/` directory of an installed tree.
4768
+ *
4769
+ * A base with no patch file is REFUSED with every probed candidate listed: composing a family preset
4770
+ * from another base's rows would mount rows the user did not ask for, with nothing saying so.
4771
+ * @param base - the base name, already validated against `evolution-agent/bases.json`.
4772
+ * @param sources - the roots to probe.
4773
+ * @returns the patch file's absolute path.
4774
+ * @throws {Error} when no candidate exists.
4775
+ */
4776
+ function resolveBasePresetPatch(base, sources) {
4777
+ const file = `${base}.patch.yml`;
4778
+ const explicit = sources.root?.trim();
4779
+ if (explicit !== void 0 && explicit !== "") {
4780
+ const path = join(explicit, file);
4781
+ if (!existsSync(path)) throw new Error(`evolution preset composition: DSH_AGENT_PRESET_ROOT is set but ${path} does not exist — ${AGENT_PRESET_ROOT_HINT}`);
4782
+ return path;
4783
+ }
4784
+ const probes = [];
4785
+ const probe = (path) => {
4786
+ probes.push(path);
4787
+ return existsSync(path) ? path : void 0;
4788
+ };
4789
+ if (sources.resolve !== void 0) {
4790
+ const probeSpecifier = `${PLATFORM_BUNDLE_PACKAGE}/presets/${file}`;
4791
+ try {
4792
+ const found = probe(sources.resolve(probeSpecifier));
4793
+ if (found !== void 0) return found;
4794
+ } catch {
4795
+ probes.push(`node module graph: ${probeSpecifier}`);
4796
+ }
4797
+ }
4798
+ if (sources.profileDir !== void 0 && sources.profileDir !== "") for (const name of new Set([PLATFORM_BUNDLE_PACKAGE, ...sources.bundles ?? []])) {
4799
+ const found = probe(join(sources.profileDir, "node_modules", ...name.split("/"), "presets", file));
4800
+ if (found !== void 0) return found;
4801
+ }
4802
+ for (let level = sources.fromDir; level !== dirname(level); level = dirname(level)) {
4803
+ const bundleRoot = join(level, "packages", "bundle");
4804
+ for (const name of listDirectories(bundleRoot)) {
4805
+ const found = probe(join(bundleRoot, name, "presets", file));
4806
+ if (found !== void 0) return found;
4807
+ }
4808
+ const scopeRoot = join(level, "node_modules", "@deepseek-ai");
4809
+ for (const name of listDirectories(scopeRoot)) {
4810
+ if (!name.startsWith("dsh-")) continue;
4811
+ const found = probe(join(scopeRoot, name, "presets", file));
4812
+ if (found !== void 0) return found;
4813
+ }
4814
+ }
4815
+ for (const root of installRoots(sources.fromDir)) for (const shape of DESKTOP_RESOURCE_SHAPES) {
4816
+ const found = probe(join(root, ...shape, "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", file));
4817
+ if (found !== void 0) return found;
4818
+ const scanned = probeDesktopScope(join(root, ...shape), file, probes);
4819
+ if (scanned !== void 0) return scanned;
4820
+ }
4821
+ throw new Error(`evolution preset composition: cannot find the runtime platform '${base}' preset patch — no candidate carries presets/${file}. A base with no runtime patch is refused rather than composed from another base: the generated preset must follow the platform it runs on. Probed (in order): ${probes.join(", ")}. On a desktop install the platform packages live inside resources/app.asar and no plain file exists — ${AGENT_PRESET_ROOT_HINT}`);
4822
+ }
4823
+ /** The Electron resource layouts a desktop installation exposes its unpacked packages under. */
4824
+ const DESKTOP_RESOURCE_SHAPES = [["resources", "app.asar.unpacked"], [
4825
+ "resources",
4826
+ "app",
4827
+ "resources",
4828
+ "app.asar.unpacked"
4829
+ ]];
4830
+ /**
4831
+ * Installation directories a desktop/Electron tree may sit at, probed before the source walk.
4832
+ *
4833
+ * The default Windows desktop location plus every ancestor that carries an
4834
+ * `resources/app.asar.unpacked` directory (a script run from inside an installation).
4835
+ * @param fromDir - the caller's own directory.
4836
+ * @returns the candidate installation roots.
4837
+ */
4838
+ function installRoots(fromDir) {
4839
+ const roots = [];
4840
+ const localAppData = process.env.LOCALAPPDATA;
4841
+ if (process.platform === "win32" && localAppData !== void 0 && localAppData !== "") roots.push(join(localAppData, "Programs", "DeepSeek Harness"));
4842
+ for (let level = fromDir; level !== dirname(level); level = dirname(level)) if (existsSync(join(level, "resources", "app.asar.unpacked"))) roots.push(level);
4843
+ return roots;
4844
+ }
4845
+ /**
4846
+ * Probe every `@deepseek-ai/dsh-*` package under one unpacked resource root.
4847
+ * @param resourceRoot - e.g. `<install>/resources/app.asar.unpacked`.
4848
+ * @param file - the patch file name to look for.
4849
+ * @param probes - the caller's probe log, appended to for the failure report.
4850
+ * @returns the patch path when one exists.
4851
+ */
4852
+ function probeDesktopScope(resourceRoot, file, probes) {
4853
+ const scopeRoot = join(resourceRoot, "dsh", "node_modules", "@deepseek-ai");
4854
+ for (const name of listDirectories(scopeRoot)) {
4855
+ if (!name.startsWith("dsh-")) continue;
4856
+ const path = join(scopeRoot, name, "presets", file);
4857
+ probes.push(path);
4858
+ if (existsSync(path)) return path;
4859
+ }
4860
+ }
4861
+ /**
4862
+ * Subdirectories of one directory, or none when it does not exist: a missing level is an ordinary
4863
+ * miss on a candidate walk, not a failure.
4864
+ * @param dir - the directory to list.
4865
+ * @returns the directory names.
4866
+ */
4867
+ function listDirectories(dir) {
4868
+ try {
4869
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
4870
+ } catch {
4871
+ return [];
4872
+ }
4873
+ }
4874
+ /**
4875
+ * The block of one row id: the enclosing column-0 list item and the row's own first line.
4876
+ *
4877
+ * An installer-written row sits inside `- insert:` (four spaces deeper); a row the Web editor
4878
+ * saved is that column-0 item itself. Both must be replaced whole, so the span reaches back to the
4879
+ * enclosing item and forward to the next one.
4880
+ * @param lines - the patch text's lines.
4881
+ * @param rowId - the row id to locate.
4882
+ * @returns the span, or null when the patch carries no such row.
4883
+ */
4884
+ function rowBlockSpan(lines, rowId) {
4885
+ const rowRe = new RegExp("^\\s*- id:\\s*" + rowId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*$");
4886
+ const rowStart = lines.findIndex((line) => rowRe.test(line));
4887
+ if (rowStart < 0) return null;
4888
+ let start = rowStart;
4889
+ while (start > 0 && !/^- /.test(lines[start] ?? "")) start -= 1;
4890
+ let end = lines.length;
4891
+ for (let index = start + 1; index < lines.length; index += 1) if (/^- /.test(lines[index] ?? "")) {
4892
+ end = index;
4893
+ break;
4894
+ }
4895
+ return {
4896
+ start,
4897
+ end,
4898
+ rowStart
4899
+ };
4900
+ }
4901
+ /**
4902
+ * Remove one row from a patch, taking its `- insert:` item with it when that item holds nothing
4903
+ * else (an empty insert entry is left-over structure the platform would keep parsing).
4904
+ * @param patchText - the patch text to edit.
4905
+ * @param rowId - the row id to remove.
4906
+ * @returns the patch text, newline-terminated; byte-identical when the id is absent.
4907
+ */
4908
+ function removePresetRow(patchText, rowId) {
4909
+ const lines = patchText.split("\n");
4910
+ const span = rowBlockSpan(lines, rowId);
4911
+ if (span === null) return patchText;
4912
+ const rowLine = lines[span.rowStart] ?? "";
4913
+ const rowIndent = rowLine.length - rowLine.trimStart().length;
4914
+ const nested = rowIndent > 0;
4915
+ const siblings = lines.slice(span.start + 1, span.end).filter((line) => {
4916
+ return line.length - line.trimStart().length === rowIndent && line.trimStart().startsWith("- ");
4917
+ }).length;
4918
+ const cut = nested && siblings > 1 ? {
4919
+ start: span.rowStart,
4920
+ end: rowSiblingEnd(lines, span.rowStart, span.end)
4921
+ } : span;
4922
+ const kept = [...lines.slice(0, cut.start), ...lines.slice(cut.end)];
4923
+ while (kept.length > 1 && (kept[kept.length - 1] ?? "") === "") kept.pop();
4924
+ const text = kept.join("\n");
4925
+ if (text === "") return "";
4926
+ return text.endsWith("\n") ? text : text + "\n";
4927
+ }
4928
+ /**
4929
+ * The end of one nested row's own block inside a patch entry.
4930
+ * @param lines - the patch text's lines.
4931
+ * @param rowStart - the row's `- id:` line.
4932
+ * @param itemEnd - the enclosing item's end.
4933
+ * @returns the index of the next sibling row, or the item's end.
4934
+ */
4935
+ function rowSiblingEnd(lines, rowStart, itemEnd) {
4936
+ const indent = (lines[rowStart] ?? "").length - (lines[rowStart] ?? "").trimStart().length;
4937
+ for (let index = rowStart + 1; index < itemEnd; index += 1) {
4938
+ const line = lines[index] ?? "";
4939
+ if (line.trim() === "") continue;
4940
+ const width = line.length - line.trimStart().length;
4941
+ if (width === indent && line.trimStart().startsWith("- ")) return index;
4942
+ if (width < indent) return index;
4943
+ }
4944
+ return itemEnd;
4945
+ }
4946
+ /**
4947
+ * Merge one generated row into a composition (a profile patch) by row id.
4948
+ *
4949
+ * Idempotent by construction: an existing row with the same `- id:` is REPLACED whole — its block
4950
+ * is the enclosing column-0 list item, so an installer-written row inside `- insert:` and a
4951
+ * Web-editor-saved row at column 0 are both matched by {@link rowBlockSpan} — and anything else in
4952
+ * the patch stays byte-identical, because the patch usually carries rows this installer knows
4953
+ * nothing about. A missing row is appended.
4954
+ *
4955
+ * The platform seeds a fresh profile patch with the empty list `[]`
4956
+ * (`packages/boot/app-boot/src/profile.ts`, `PROFILE_PATCH_FILENAME`), which is a COMPLETE YAML
4957
+ * document: a block sequence appended after it is not parsable. That seed is therefore replaced, not
4958
+ * extended.
4959
+ * @param patchText - the current patch text (`''` for a fresh one).
4960
+ * @param row - the row text, as {@link composePresetRow} returns it.
4961
+ * @param rowId - the row id to replace or append.
4962
+ * @returns the merged patch text, newline-terminated.
4963
+ */
4964
+ function mergePresetRow(patchText, row, rowId) {
4965
+ const lines = patchText.split("\n");
4966
+ const block = row.replace(/\s+$/, "").split("\n");
4967
+ const span = rowBlockSpan(lines, rowId);
4968
+ if (span === null) {
4969
+ const seedIndex = lines.findIndex((line) => EMPTY_PATCH_SEED_RE.test(line));
4970
+ const kept = trimTrailingBlankLines(seedIndex >= 0 ? [...lines.slice(0, seedIndex), ...lines.slice(seedIndex + 1)] : [...lines]);
4971
+ return (kept.length === 0 ? block : [...kept, ...block]).join("\n") + "\n";
4972
+ }
4973
+ return trimTrailingBlankLines([
4974
+ ...lines.slice(0, span.start),
4975
+ ...block,
4976
+ ...lines.slice(span.end)
4977
+ ]).join("\n") + "\n";
4978
+ }
4979
+ /** The empty patch list the platform seeds a profile patch with; see {@link mergePresetRow}. */
4980
+ const EMPTY_PATCH_SEED = "[]";
4981
+ /** The seed in any spelling the platform accepts (`[]`, `[ ]`, `[] # empty`). */
4982
+ const EMPTY_PATCH_SEED_RE = /^\s*\[\s*\](\s*#.*)?\s*$/;
4983
+ /** Drop trailing blank lines only: a kept line's own terminator (a CRLF file's `\r`) is never touched. */
4984
+ function trimTrailingBlankLines(lines) {
4985
+ const out = [...lines];
4986
+ while (out.length > 0 && (out[out.length - 1] ?? "").trim() === "") out.pop();
4987
+ return out;
4988
+ }
4989
+ /**
4990
+ * The text to write when a patch is left with no entries.
4991
+ *
4992
+ * A zero-byte patch file is not what the platform writes or parses; its own seed is the empty list.
4993
+ * @param patchText - the text {@link removePresetRow} returned.
4994
+ * @returns the platform's empty-list seed when nothing is left, the text otherwise.
4995
+ */
4996
+ function presetPatchText(patchText) {
4997
+ return patchText.trim() === "" ? `[]\n` : patchText;
4998
+ }
4576
4999
  //#endregion
4577
5000
  //#region lib/types/prompts.js
4578
5001
  /**
@@ -5753,6 +6176,47 @@ function readDispatchRecord(event) {
5753
6176
  };
5754
6177
  }
5755
6178
  /**
6179
+ * The ledger key of one dispatch: a scope-qualified call id.
6180
+ *
6181
+ * v43 audit (FLOW4-4): a normalizer shared by every session (skill-usage's live listener holds ONE
6182
+ * process-wide instance) used the bare call id, so two sessions that produced the same id — PTC
6183
+ * sub-call ids are short, and an id-less payload falls back to a type+payload key that is not
6184
+ * unique by construction — collided: the second session's read was absorbed as "already seen" and
6185
+ * never counted, and a settle in one session flipped the other's `ok`. Callers that span sessions
6186
+ * pass the session id as `scope`.
6187
+ * @param callId - the dispatch's call id (or its payload-derived fallback).
6188
+ * @param scope - the owning session id, or '' for a fold that never leaves one session.
6189
+ * @returns the key both the ledger and every projection fold dedupe on.
6190
+ */
6191
+ function dispatchKeyOf(callId, scope = "") {
6192
+ return scope === "" ? callId : `${scope}:${callId}`;
6193
+ }
6194
+ /**
6195
+ * The pure half of the ledger: what one event says about one dispatch.
6196
+ *
6197
+ * The DECISION (which dispatch, and whether this event is the one that opens it) lives here and
6198
+ * only here; carriers differ in what they keep — the class below keeps consumer-visible mutable
6199
+ * signals, the family's `evolutionReads`/`evolutionSignals` projection units keep plain-JSON
6200
+ * ledgers. `name` is empty on an event that only settles an already-opened dispatch (a native
6201
+ * `tool/result`), and `outcome` is present exactly when the event carried one.
6202
+ * @param event - one session event; any non-dispatch event answers `null`.
6203
+ * @param scope - the owning session id, or '' for a single-session fold.
6204
+ * @returns the step, or `null` when the event is not a dispatch.
6205
+ */
6206
+ function dispatchStepOf(event, scope = "") {
6207
+ const record = readDispatchRecord(event);
6208
+ if (record === null) return null;
6209
+ return {
6210
+ key: dispatchKeyOf(record.callId, scope),
6211
+ kind: record.kind,
6212
+ callId: record.callId,
6213
+ rootCallId: record.rootCallId,
6214
+ name: record.name,
6215
+ arguments: record.arguments,
6216
+ ...record.outcome === void 0 ? {} : { outcome: record.outcome }
6217
+ };
6218
+ }
6219
+ /**
5756
6220
  * The family's single dispatch ledger: absorbs platform events in log order,
5757
6221
  * emits one \`ToolDispatchSignal\` per dispatch, and folds later events of the
5758
6222
  * same dispatch into the record it already emitted.
@@ -5776,18 +6240,6 @@ var ToolDispatchNormalizer = class {
5776
6240
  this.maxTracked = options.maxTracked ?? Number.POSITIVE_INFINITY;
5777
6241
  }
5778
6242
  /**
5779
- * v43 audit (FLOW4-4): the ledger key. A normalizer shared by every session
5780
- * (skill-usage's live listener holds ONE process-wide instance) used the bare
5781
- * call id, so two sessions that produced the same id — PTC sub-call ids are
5782
- * short, and an id-less payload falls back to a type+payload key that is not
5783
- * unique by construction — collided: the second session's read was absorbed as
5784
- * "already seen" and never counted, and a settle in one session flipped the
5785
- * other's `ok`. Callers that span sessions pass the session id as `scope`.
5786
- */
5787
- keyOf(callId, scope) {
5788
- return scope === "" ? callId : `${scope}:${callId}`;
5789
- }
5790
- /**
5791
6243
  * Absorb one session event.
5792
6244
  * @param event - the event to absorb; any non-dispatch event is ignored.
5793
6245
  * @returns the dispatch's signal when this event FIRST reveals the dispatch,
@@ -5795,28 +6247,23 @@ var ToolDispatchNormalizer = class {
5795
6247
  * non-dispatch event). A \`null\` return is never a dispatch to count again.
5796
6248
  */
5797
6249
  advance(event, scope = "") {
5798
- const record = readDispatchRecord(event);
5799
- if (record === null) return null;
5800
- const key = this.keyOf(record.callId, scope);
5801
- if (record.name === "") {
5802
- const existing = this.records.get(key);
5803
- if (existing !== void 0 && record.outcome !== void 0) existing.ok = record.outcome.ok;
5804
- return null;
5805
- }
5806
- const existing = this.records.get(key);
6250
+ const step = dispatchStepOf(event, scope);
6251
+ if (step === null) return null;
6252
+ const existing = this.records.get(step.key);
5807
6253
  if (existing !== void 0) {
5808
- if (record.outcome !== void 0) existing.ok = record.outcome.ok;
6254
+ if (step.outcome !== void 0) existing.ok = step.outcome.ok;
5809
6255
  return null;
5810
6256
  }
6257
+ if (step.name === "") return null;
5811
6258
  const signal = {
5812
- kind: record.kind,
5813
- callId: record.callId,
5814
- rootCallId: record.rootCallId,
5815
- name: record.name,
5816
- arguments: record.arguments,
5817
- ok: record.outcome?.ok
6259
+ kind: step.kind,
6260
+ callId: step.callId,
6261
+ rootCallId: step.rootCallId,
6262
+ name: step.name,
6263
+ arguments: step.arguments,
6264
+ ok: step.outcome?.ok
5818
6265
  };
5819
- this.records.set(key, signal);
6266
+ this.records.set(step.key, signal);
5820
6267
  this.evict();
5821
6268
  return signal;
5822
6269
  }
@@ -5831,13 +6278,12 @@ var ToolDispatchNormalizer = class {
5831
6278
  * @returns the dispatch this event settled, or \`null\`.
5832
6279
  */
5833
6280
  settledSignalOf(event, scope = "") {
5834
- const record = readDispatchRecord(event);
5835
- if (record === null || record.outcome === void 0) return null;
5836
- const key = this.keyOf(record.callId, scope);
5837
- if (this.settledIds.has(key)) return null;
5838
- const signal = this.records.get(key);
6281
+ const step = dispatchStepOf(event, scope);
6282
+ if (step?.outcome === void 0) return null;
6283
+ if (this.settledIds.has(step.key)) return null;
6284
+ const signal = this.records.get(step.key);
5839
6285
  if (signal === void 0) return null;
5840
- this.settledIds.add(key);
6286
+ this.settledIds.add(step.key);
5841
6287
  this.evict();
5842
6288
  return signal;
5843
6289
  }
@@ -6105,47 +6551,94 @@ function textOfBlock(block) {
6105
6551
  const candidate = block;
6106
6552
  return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
6107
6553
  }
6554
+ /** The empty turn-window fold state. */
6555
+ function initialTurnFold() {
6556
+ return {
6557
+ substantive: false,
6558
+ toolCalls: 0,
6559
+ userChars: 0,
6560
+ assistantChars: 0,
6561
+ memorySignal: false,
6562
+ skillSignal: false,
6563
+ opened: []
6564
+ };
6565
+ }
6108
6566
  /**
6109
- * Per-turn dispatch ledgers, keyed by the signal object they feed.
6567
+ * Fold ONE session event into a turn-window state, purely.
6110
6568
  *
6111
- * `observeEvent` folds ONE event at a time, so the dedup state has to outlive
6112
- * the call. One WeakMap entry per `TurnSignals`, so a finished fold's ledger is
6113
- * collectable with the fold itself and two concurrent turns never share one.
6569
+ * The one fold behind all three readers: `observeEvent` (the mutating face the live `turn/end`
6570
+ * path uses), `foldTurn` (a whole-log window fold, kept for structural test sessions) and the
6571
+ * family's `evolutionSignals` projection unit. P1-1/N4 guards stay: events arrive from disk, so
6572
+ * a persisted `data: null` / non-object or a malformed `content` skips instead of throwing.
6573
+ * @param state - the window so far.
6574
+ * @param event - the next committed session event.
6575
+ * @returns the next state; the same reference when the event carries nothing this fold reads.
6114
6576
  */
6115
- const dispatchLedgers = /* @__PURE__ */ new WeakMap();
6116
- /** The ledger folding this signal's turn, created on first use. */
6117
- function normalizerForSignal(signal) {
6118
- let normalizer = dispatchLedgers.get(signal);
6119
- if (normalizer === void 0) {
6120
- normalizer = new ToolDispatchNormalizer();
6121
- dispatchLedgers.set(signal, normalizer);
6122
- }
6123
- return normalizer;
6124
- }
6125
- /** Fold one session event into the current turn observation. */
6126
- function observeEvent(signal, event) {
6577
+ function foldTurnEvent(state, event) {
6127
6578
  const data = event.data;
6128
- if (data === null || typeof data !== "object" || Array.isArray(data)) return;
6579
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return state;
6129
6580
  if (event.type === "user/message") {
6130
6581
  const content = data.content;
6131
- if (!Array.isArray(content)) return;
6582
+ if (!Array.isArray(content)) return state;
6132
6583
  const text = content.map(textOfBlock).join(" ");
6133
- signal.userChars += text.length;
6134
- if (CORRECTION_PATTERNS.some((pattern) => pattern.test(text))) signal.memorySignal = true;
6135
- if (FIX_PATTERNS.some((pattern) => pattern.test(text))) signal.skillSignal = true;
6136
- return;
6584
+ return {
6585
+ ...state,
6586
+ userChars: state.userChars + text.length,
6587
+ memorySignal: state.memorySignal || CORRECTION_PATTERNS.some((pattern) => pattern.test(text)),
6588
+ skillSignal: state.skillSignal || FIX_PATTERNS.some((pattern) => pattern.test(text))
6589
+ };
6137
6590
  }
6138
6591
  if (event.type === "assistant/message") {
6139
6592
  const message = data.message;
6140
- if (!message || !Array.isArray(message.content)) return;
6593
+ if (!message || !Array.isArray(message.content)) return state;
6141
6594
  const text = message.content.map(textOfBlock).join(" ");
6142
- signal.assistantChars += text.length;
6143
- return;
6595
+ return {
6596
+ ...state,
6597
+ assistantChars: state.assistantChars + text.length
6598
+ };
6144
6599
  }
6145
- const dispatch = normalizerForSignal(signal).advance(event);
6146
- if (dispatch === null) return;
6147
- if (dispatch.kind !== "program") signal.toolCalls += 1;
6148
- if (isSkillToolName(dispatch.name)) signal.skillSignal = true;
6600
+ const dispatch = dispatchStepOf(event);
6601
+ if (dispatch === null || dispatch.name === "" || state.opened.includes(dispatch.key)) return state;
6602
+ let toolCalls = state.toolCalls;
6603
+ if (dispatch.kind !== "program") toolCalls += 1;
6604
+ return {
6605
+ ...state,
6606
+ opened: [...state.opened, dispatch.key],
6607
+ toolCalls,
6608
+ skillSignal: state.skillSignal || isSkillToolName(dispatch.name)
6609
+ };
6610
+ }
6611
+ /**
6612
+ * Per-turn dispatch ledgers, keyed by the signal object they feed.
6613
+ *
6614
+ * `observeEvent` folds ONE event at a time, so the ledger has to outlive the call. One WeakMap
6615
+ * entry per `TurnSignals`, so a finished fold's ledger is collectable with the fold itself and two
6616
+ * concurrent turns never share one.
6617
+ */
6618
+ const dispatchLedgers = /* @__PURE__ */ new WeakMap();
6619
+ /** The ledger of the turn this signal is folding; empty before its first dispatch. */
6620
+ function ledgerForSignal(signal) {
6621
+ return dispatchLedgers.get(signal) ?? [];
6622
+ }
6623
+ /**
6624
+ * Fold one session event into the current turn observation (the mutating face of
6625
+ * {@link foldTurnEvent}: every reader shares the one fold, this one copies the result back onto
6626
+ * the caller's signal object and keeps its ledger for the next event).
6627
+ * @param signal - the turn observation to advance.
6628
+ * @param event - the next committed session event.
6629
+ */
6630
+ function observeEvent(signal, event) {
6631
+ const next = foldTurnEvent({
6632
+ ...signal,
6633
+ opened: [...ledgerForSignal(signal)]
6634
+ }, event);
6635
+ signal.substantive = next.substantive;
6636
+ signal.toolCalls = next.toolCalls;
6637
+ signal.userChars = next.userChars;
6638
+ signal.assistantChars = next.assistantChars;
6639
+ signal.memorySignal = next.memorySignal;
6640
+ signal.skillSignal = next.skillSignal;
6641
+ dispatchLedgers.set(signal, next.opened);
6149
6642
  }
6150
6643
  /** Compute review cadence after `turn/end`. */
6151
6644
  function advanceReview(state, turn, signal, config) {
@@ -6175,60 +6668,400 @@ function advanceReview(state, turn, signal, config) {
6175
6668
  }
6176
6669
  return null;
6177
6670
  }
6178
- /** Fold all events between two sequence boundaries into one TurnSignals. */
6179
- function foldTurn(session, fromSeq) {
6180
- const signal = {
6181
- substantive: false,
6182
- toolCalls: 0,
6183
- userChars: 0,
6184
- assistantChars: 0,
6185
- memorySignal: false,
6186
- skillSignal: false
6187
- };
6188
- const events = session.snapshotEvents();
6189
- for (let index = Math.max(0, fromSeq); index < events.length; index += 1) {
6190
- const event = events[index];
6191
- if (event) observeEvent(signal, event);
6671
+ /**
6672
+ * Fold a log slice into one turn window, purely.
6673
+ *
6674
+ * The whole-log reference for {@link foldTurnEvent}: a caller that holds events (a fixture, a
6675
+ * structural view, a replay) reads a window without touching a platform Session. Production reads
6676
+ * the same window live from the family's `evolutionSignals` projection (`sessionTurnSignals`),
6677
+ * which folds `foldTurnEvent` once per committed event.
6678
+ * @param events - the log, oldest first. Pass an array, not a live iterator, when it can grow.
6679
+ * @param fromSeq - index of the first event of the window (0 for the whole log).
6680
+ * @returns the window's signals.
6681
+ */
6682
+ function foldTurnEvents(events, fromSeq = 0) {
6683
+ const list = [...events];
6684
+ let state = initialTurnFold();
6685
+ for (let index = Math.max(0, fromSeq); index < list.length; index += 1) {
6686
+ const event = list[index];
6687
+ if (event) state = foldTurnEvent(state, event);
6192
6688
  }
6193
- return signal;
6689
+ const { substantive, toolCalls, userChars, assistantChars, memorySignal, skillSignal } = state;
6690
+ return {
6691
+ substantive,
6692
+ toolCalls,
6693
+ userChars,
6694
+ assistantChars,
6695
+ memorySignal,
6696
+ skillSignal
6697
+ };
6194
6698
  }
6195
6699
  //#endregion
6196
- //#region lib/types/skill-reads.js
6700
+ //#region lib/types/evidence.js
6197
6701
  /**
6198
- * The read-before-write RULE: which writes need a prior read, and which targets count as read.
6199
- *
6200
- * ONE rule for TWO enforcement points (design `dsh-evolution-write-gate-design.md` §4.2): the
6201
- * tool path refuses an unread mutating write (E-318), and the plan path drops such an op from a
6202
- * background review's plan. Before this module the rule lived in `evolution-review` only, so the
6203
- * tool path could not see it at all.
6204
- *
6205
- * The READER is not here either: `tool-dispatch.collectReadSkillNames(events)` folds a session log
6206
- * into the names read through a non-failed dispatch — the review's old session-shaped copy was a
6207
- * line-for-line duplicate of it. What this module adds over it is the session-shaped ADAPTER
6208
- * (`sessionReadSkillNames`), for the ONE caller that holds a session only STRUCTURALLY (the tool
6209
- * path's gate, whose exec view may be a bare stub): it answers "which accessor, and what does an
6210
- * unreadable session mean" — `undefined`, so the gate keeps its previous behavior instead of
6211
- * refusing every write. The review path reads a real platform `Session` and calls the reader
6212
- * directly; a fallback there would be dead code for an impossible case.
6213
- *
6214
- * v32 REV-06(a) is preserved by the dispatch normalizer: a skill counts as READ only when its read
6215
- * did not fail, so a failed/timeout read cannot pass the gate and let a writer blind-overwrite
6216
- * content the model never saw. Matching `tool/call` directly (the pre-v37 form) meant every PTC
6217
- * session (`tool/ptc-dispatch*`) collected an EMPTY set, so every mutating op the model HAD read
6218
- * was dropped — and nothing reported the loss.
6702
+ * Which session frames are EVIDENCE, and which are bookkeeping.
6703
+ *
6704
+ * A plan op must cite `evidence` seqs, and the validator's range rule
6705
+ * (`evolution-plan-validator`, rule `EVIDENCE_RANGE`) answers only "is this an integer inside
6706
+ * the window the plan was authored against". A boundary frame satisfies that while carrying
6707
+ * nothing a claim can rest on: the `turn/start` seq is the frame a claim about that turn was
6708
+ * most likely read out of, and citing it is indistinguishable from citing a real exchange. This
6709
+ * module owns the ONE classification of a session frame by what it CARRIES, so the plan path can
6710
+ * report an op whose whole citation list is bookkeeping (design
6711
+ * `dsh-evolution-skill-history-design.md` §3D, phase 1: report only — no op is refused here).
6712
+ *
6713
+ * The classification is a DENY list of the pure bookends, not an allow list of content frames.
6714
+ * The platform's own vocabulary (`KNOWN_SESSION_EVENT_TYPES`, 57 members at 0.1.5-rc.2) mixes
6715
+ * content frames with state-change frames, and out-of-repo plugins append types the family cannot
6716
+ * enumerate; an unknown frame is therefore treated as carrying content, because a report that
6717
+ * fires on an unclassifiable frame is a false positive and phase 1 exists to measure how many TRUE
6718
+ * ones there are. Tightening — a narrower definition, or an allow list built from the platform's
6719
+ * vocabulary — changes this table and nothing else (`evidenceKindIndex` is the only reader).
6720
+ * @module @lmzhen/dsh-evolution-core
6721
+ */
6722
+ /** The frame types that record a boundary and carry no content: the turn and step bookends. */
6723
+ const BOOKKEEPING_FRAME_TYPES = new Set([
6724
+ "turn/start",
6725
+ "turn/end",
6726
+ "step/start",
6727
+ "step/end"
6728
+ ]);
6729
+ /**
6730
+ * Whether one frame type records a boundary and carries no content.
6731
+ *
6732
+ * The classification table's SECOND reader: the live projection unit
6733
+ * (`session-projection.ts`) folds the same answer per event, so the rule stays one table.
6734
+ * @param type - a platform session-event type.
6735
+ * @returns true for the turn/step bookends.
6736
+ */
6737
+ function isBookkeepingFrame(type) {
6738
+ return BOOKKEEPING_FRAME_TYPES.has(type);
6739
+ }
6740
+ /**
6741
+ * The seqs of the frames that carry content.
6742
+ *
6743
+ * @param events - the session's frames, oldest first (any iterable; nothing is retained).
6744
+ * @returns the substantive seqs, or `undefined` when NO frame carried a usable seq. The two
6745
+ * answers are different facts: an empty SET says "every sequenced frame is a boundary", while
6746
+ * `undefined` says "this log cannot tell a boundary from a message" (a stub session in a test,
6747
+ * a composition whose events carry no seq). A caller that cannot classify must stay silent
6748
+ * rather than report every op — the same three-state posture as `sessionReadSkillNames`.
6749
+ */
6750
+ function evidenceKindIndex(events) {
6751
+ const substantive = /* @__PURE__ */ new Set();
6752
+ let sequenced = 0;
6753
+ for (const event of events) {
6754
+ const seq = event.seq;
6755
+ if (typeof seq !== "number" || !Number.isInteger(seq)) continue;
6756
+ sequenced += 1;
6757
+ if (typeof event.type === "string" && BOOKKEEPING_FRAME_TYPES.has(event.type)) continue;
6758
+ substantive.add(seq);
6759
+ }
6760
+ return sequenced === 0 ? void 0 : substantive;
6761
+ }
6762
+ //#endregion
6763
+ //#region lib/types/session-projection.js
6764
+ /**
6765
+ * The family's session projections: the per-session facts the family used to re-read the log for.
6766
+ *
6767
+ * 0.2.x deprecates the synchronous log reads (`Session.snapshotEvents` — "existing logic may
6768
+ * remain unmigrated for now, but new calls are prohibited",
6769
+ * `core/session/src/index.ts:639-658`) and drives DOMAIN-OWNED units instead: a registered unit
6770
+ * folds every committed event once, the registry owns the subscription and the per-session
6771
+ * watermark, and a reader asks for the current state
6772
+ * (`session/session-projection/src/index.ts` — `register`/`stateOf`). Each unit here carries
6773
+ * exactly what one pure family reader derives from a log, so that reader keeps its pure form and
6774
+ * gains a projection-backed one beside it.
6775
+ *
6776
+ * State stays bounded. The evidence unit keeps the SPARSE boundary seqs and answers the dense
6777
+ * question by complement: a content-seq set grows with every session event, while a turn/step
6778
+ * boundary set grows four entries per turn.
6779
+ *
6780
+ * The registry is part of the platform's BASE bundle (`packages/bundle/base/cordis.patch.yml:158`),
6781
+ * so a real host always has it; a composition without it keeps loading (`ctx.inject` probe) and
6782
+ * its readers report "cannot classify" instead of guessing — the same posture the pure readers
6783
+ * document for a log with no seq.
6219
6784
  * @module @lmzhen/dsh-evolution-core
6220
6785
  */
6786
+ const MAX_TIMESTAMP_MS = 864e13;
6787
+ const activityStateSchema = z.object({ lastEventTime: z.number().nonnegative().max(MAX_TIMESTAMP_MS) }).strict();
6788
+ const evidenceStateSchema = z.object({
6789
+ sequenced: z.number().int().nonnegative(),
6790
+ through: z.number().int().min(-1).max(Number.MAX_SAFE_INTEGER),
6791
+ bookkeeping: z.array(z.number().int().nonnegative())
6792
+ }).strict();
6793
+ /** The `evolutionActivity` unit (host-only: no client view is registered for it). */
6794
+ const evolutionActivityProjection = {
6795
+ key: "evolutionActivity",
6796
+ stateVersion: 1,
6797
+ stateSchema: activityStateSchema,
6798
+ init: () => ({ lastEventTime: 0 }),
6799
+ apply: (state, event) => {
6800
+ const time = typeof event.time === "number" ? event.time : state.lastEventTime;
6801
+ return time === state.lastEventTime ? state : { lastEventTime: time };
6802
+ }
6803
+ };
6804
+ /** The `evolutionEvidence` unit (host-only: no client view is registered for it). */
6805
+ const evolutionEvidenceProjection = {
6806
+ key: "evolutionEvidence",
6807
+ stateVersion: 1,
6808
+ stateSchema: evidenceStateSchema,
6809
+ init: () => ({
6810
+ sequenced: 0,
6811
+ through: -1,
6812
+ bookkeeping: []
6813
+ }),
6814
+ apply: (state, event) => {
6815
+ const seq = event.seq;
6816
+ if (typeof seq !== "number" || !Number.isInteger(seq)) return state;
6817
+ const sequenced = state.sequenced + 1;
6818
+ const through = seq > state.through ? seq : state.through;
6819
+ if (typeof event.type !== "string" || !isBookkeepingFrame(event.type)) return {
6820
+ sequenced,
6821
+ through,
6822
+ bookkeeping: state.bookkeeping
6823
+ };
6824
+ return {
6825
+ sequenced,
6826
+ through,
6827
+ bookkeeping: [...state.bookkeeping, seq]
6828
+ };
6829
+ }
6830
+ };
6831
+ /**
6832
+ * Whether one session object can be projected at all.
6833
+ *
6834
+ * The family's readers also take STRUCTURAL session views — `skill-reads.ts` documents the tool
6835
+ * path's bare stub — while the registry folds a session's own log (`snapshotEvents(from, to)`,
6836
+ * `eventAt(seq)`, `seq`, `inheritedEventCount`) whenever it has to build or advance a cell. A
6837
+ * view without those members has no projection state to read, and asking for one would fold a
6838
+ * log the unit cannot index.
6839
+ * @param session - the value a caller holds as its session.
6840
+ * @returns true when the registry can fold this object.
6841
+ */
6842
+ function isProjectableSession(session) {
6843
+ if (session === null || typeof session !== "object") return false;
6844
+ const candidate = session;
6845
+ const folds = typeof candidate.eventAt === "function";
6846
+ const logs = typeof candidate.snapshotEvents === "function";
6847
+ return folds && logs && typeof candidate.seq === "number";
6848
+ }
6849
+ const readsStateSchema = z.object({ reads: z.array(z.object({
6850
+ key: z.string(),
6851
+ skill: z.string().nullable(),
6852
+ failed: z.boolean()
6853
+ }).strict()) }).strict();
6854
+ const signalsStateSchema = z.object({
6855
+ through: z.number().int().min(-1).max(Number.MAX_SAFE_INTEGER),
6856
+ lastEventTime: z.number().nonnegative().max(MAX_TIMESTAMP_MS),
6857
+ turnStartSeq: z.number().int().min(-1).max(Number.MAX_SAFE_INTEGER),
6858
+ turn: z.object({
6859
+ substantive: z.boolean(),
6860
+ toolCalls: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
6861
+ userChars: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
6862
+ assistantChars: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
6863
+ memorySignal: z.boolean(),
6864
+ skillSignal: z.boolean(),
6865
+ opened: z.array(z.string())
6866
+ }).strict()
6867
+ }).strict();
6868
+ /** The `evolutionReads` unit (host-only: no client view is registered for it). */
6869
+ const evolutionReadsProjection = {
6870
+ key: "evolutionReads",
6871
+ stateVersion: 1,
6872
+ stateSchema: readsStateSchema,
6873
+ init: () => ({ reads: [] }),
6874
+ apply: (state, event) => {
6875
+ const step = dispatchStepOf(event);
6876
+ if (step === null) return state;
6877
+ const at = state.reads.findIndex((entry) => entry.key === step.key);
6878
+ const settled = () => {
6879
+ const entry = at < 0 ? void 0 : state.reads[at];
6880
+ if (entry === void 0 || step.outcome === void 0) return state;
6881
+ const failed = !step.outcome.ok;
6882
+ if (entry.failed === failed) return state;
6883
+ const reads = [...state.reads];
6884
+ reads[at] = {
6885
+ key: entry.key,
6886
+ skill: entry.skill,
6887
+ failed
6888
+ };
6889
+ return { reads };
6890
+ };
6891
+ if (step.name === "") return settled();
6892
+ if (at >= 0) return settled();
6893
+ if (!isSkillReadToolName(step.name)) return state;
6894
+ const skill = skillReadNameOf({
6895
+ kind: step.kind,
6896
+ callId: step.callId,
6897
+ rootCallId: step.rootCallId,
6898
+ name: step.name,
6899
+ arguments: step.arguments,
6900
+ ok: step.outcome?.ok
6901
+ }) ?? null;
6902
+ return { reads: [...state.reads, {
6903
+ key: step.key,
6904
+ skill,
6905
+ failed: step.outcome?.ok === false
6906
+ }] };
6907
+ }
6908
+ };
6909
+ /** The `evolutionSignals` unit (host-only: no client view is registered for it). */
6910
+ const evolutionSignalsProjection = {
6911
+ key: "evolutionSignals",
6912
+ stateVersion: 1,
6913
+ stateSchema: signalsStateSchema,
6914
+ init: () => ({
6915
+ through: -1,
6916
+ lastEventTime: 0,
6917
+ turnStartSeq: -1,
6918
+ turn: initialTurnFold()
6919
+ }),
6920
+ apply: (state, event) => {
6921
+ const start = event.type === "turn/start";
6922
+ const turn = foldTurnEvent(start ? initialTurnFold() : state.turn, event);
6923
+ const seq = typeof event.seq === "number" && Number.isInteger(event.seq) ? event.seq : state.through;
6924
+ const time = typeof event.time === "number" ? event.time : state.lastEventTime;
6925
+ const turnStartSeq = start ? seq : state.turnStartSeq;
6926
+ if (turn === state.turn && seq === state.through && time === state.lastEventTime && turnStartSeq === state.turnStartSeq) return state;
6927
+ return {
6928
+ through: seq,
6929
+ lastEventTime: time,
6930
+ turnStartSeq,
6931
+ turn
6932
+ };
6933
+ }
6934
+ };
6935
+ /**
6936
+ * Register the family's units on the calling context's fiber.
6937
+ *
6938
+ * Called by the READERS (not by a plugin's mount) and idempotent by the registry's own reference
6939
+ * count: the first read of a foldable session installs the definitions, later reads count as
6940
+ * co-owners, and the keys survive until the last registrant unloads
6941
+ * (`session-projection/src/index.ts:273-292`). Registering at the read is what keeps a session
6942
+ * view the registry cannot fold (`isProjectableSession`) from ever reaching its drive — the
6943
+ * registry folds the in-memory log on first touch, so a unit that appears mid-session is still
6944
+ * correct (`:602-630`).
6945
+ *
6946
+ * Idempotent by design, so a plugin mounting later does not need its own call.
6947
+ * @param ctx - the mounting plugin's context.
6948
+ */
6949
+ function installEvolutionProjections(ctx) {
6950
+ const register = (registry) => {
6951
+ registry.register(evolutionActivityProjection);
6952
+ registry.register(evolutionEvidenceProjection);
6953
+ registry.register(evolutionReadsProjection);
6954
+ registry.register(evolutionSignalsProjection);
6955
+ };
6956
+ const mounted = ctx.get("sessionProjections");
6957
+ if (mounted !== void 0) {
6958
+ register(mounted);
6959
+ return;
6960
+ }
6961
+ ctx.inject(["sessionProjections"], (injected) => {
6962
+ register(injected.sessionProjections);
6963
+ });
6964
+ }
6965
+ /**
6966
+ * Time of one session's newest event, or `undefined` when the unit is not installed.
6967
+ *
6968
+ * The curator's idleness gate reads this instead of folding the session log; `undefined` is a
6969
+ * measurement that did not happen, which the gate answers with the deployment's fail-open policy.
6970
+ * @param ctx - a context of the runtime (the service is read from the global store).
6971
+ * @param session - the session to ask about.
6972
+ * @returns the newest event's milliseconds, or undefined with no unit or no event.
6973
+ */
6974
+ function sessionLastEventTime(ctx, session) {
6975
+ if (!isProjectableSession(session)) return void 0;
6976
+ installEvolutionProjections(ctx);
6977
+ const state = ctx.get("sessionProjections")?.stateOf(session, "evolutionActivity");
6978
+ if (state === void 0 || state.lastEventTime === 0) return void 0;
6979
+ return state.lastEventTime;
6980
+ }
6981
+ /**
6982
+ * The skill names one session read through a non-failed dispatch, or `undefined` when the session
6983
+ * cannot be projected (a structural view, or a host without the registry).
6984
+ *
6985
+ * `undefined` is NOT an empty set: the tool path's read-before-write gate keeps its previous
6986
+ * behavior rather than refusing every write in a composition whose session object is a bare stub
6987
+ * (`skill-reads.ts` documents that three-state posture).
6988
+ * @param ctx - a context of the runtime.
6989
+ * @param session - the session whose reads are wanted.
6990
+ * @returns the names read, or undefined when the log cannot be folded.
6991
+ */
6992
+ function sessionReadNames(ctx, session) {
6993
+ if (!isProjectableSession(session)) return void 0;
6994
+ installEvolutionProjections(ctx);
6995
+ const state = ctx.get("sessionProjections")?.stateOf(session, "evolutionReads");
6996
+ if (state === void 0) return void 0;
6997
+ const names = /* @__PURE__ */ new Set();
6998
+ for (const entry of state.reads) if (!entry.failed && entry.skill !== null) names.add(entry.skill);
6999
+ return names;
7000
+ }
7001
+ /**
7002
+ * The open turn's window signals, or `undefined` when the session cannot be projected.
7003
+ *
7004
+ * The window is the events since the newest `turn/start`, folded by the same `foldTurnEvent`
7005
+ * `observeEvent` uses; `undefined` means the caller has no measurement (a structural view), not
7006
+ * that the turn was empty.
7007
+ * @param ctx - a context of the runtime.
7008
+ * @param session - the session whose open turn is being judged.
7009
+ * @returns the window signals, or undefined when the log cannot be folded.
7010
+ */
7011
+ function sessionTurnSignals(ctx, session) {
7012
+ if (!isProjectableSession(session)) return void 0;
7013
+ installEvolutionProjections(ctx);
7014
+ const state = ctx.get("sessionProjections")?.stateOf(session, "evolutionSignals");
7015
+ if (state === void 0) return void 0;
7016
+ const { substantive, toolCalls, userChars, assistantChars, memorySignal, skillSignal } = state.turn;
7017
+ return {
7018
+ substantive,
7019
+ toolCalls,
7020
+ userChars,
7021
+ assistantChars,
7022
+ memorySignal,
7023
+ skillSignal
7024
+ };
7025
+ }
7026
+ /**
7027
+ * The evidence index of one session, or `undefined` when the log cannot be classified.
7028
+ *
7029
+ * `undefined` covers both the missing unit and a log whose frames carry no seq: the plan path's
7030
+ * `EVIDENCE_CLASS` row is report-only and must stay silent rather than report every op.
7031
+ * @param ctx - a context of the runtime.
7032
+ * @param session - the session the review plan is authored against.
7033
+ * @returns an index answering "does this seq carry content?".
7034
+ */
7035
+ function sessionEvidenceIndex(ctx, session) {
7036
+ if (!isProjectableSession(session)) return void 0;
7037
+ installEvolutionProjections(ctx);
7038
+ const state = ctx.get("sessionProjections")?.stateOf(session, "evolutionEvidence");
7039
+ if (state === void 0 || state.sequenced === 0) return void 0;
7040
+ const { through, bookkeeping } = state;
7041
+ return { has: (seq) => Number.isInteger(seq) && seq >= 0 && seq <= through && !bookkeeping.includes(seq) };
7042
+ }
7043
+ //#endregion
7044
+ //#region lib/types/skill-reads.js
6221
7045
  /**
6222
7046
  * The skill names ONE session read, or `undefined` when its log is not readable.
6223
7047
  *
7048
+ * Two readers, in this order: the family's `evolutionReads` projection for a platform `Session`
7049
+ * (0.2.x's live read — the deprecated synchronous log read is gone from production), then the
7050
+ * STRUCTURAL accessor for a view that is not a `Session` at all. The tool path's exec view may be a
7051
+ * bare stub, which is what this module exists for; a real Session always takes the first path.
7052
+ *
6224
7053
  * `undefined` is NOT an empty set: a caller that must decide (the tool path's read-before-write
6225
7054
  * gate) can then keep its previous behavior instead of refusing every write in a composition whose
6226
7055
  * session objects expose no log at all.
7056
+ * @param ctx - a context of the runtime.
6227
7057
  * @param session - the session whose reads are wanted; `undefined` when the call carries none.
6228
7058
  * @returns the names read through a non-failed `skill` dispatch, or `undefined` when unreadable.
6229
7059
  */
6230
- function sessionReadSkillNames(session) {
6231
- const events = session?.snapshotEvents?.();
7060
+ function sessionReadSkillNames(ctx, session) {
7061
+ if (session === void 0) return void 0;
7062
+ const projected = sessionReadNames(ctx, session);
7063
+ if (projected !== void 0) return projected;
7064
+ const events = session.snapshotEvents?.();
6232
7065
  return events === void 0 ? void 0 : collectReadSkillNames(events);
6233
7066
  }
6234
7067
  /** Actions whose target must have been read first (create is authorship, so it is exempt). */
@@ -6481,58 +7314,6 @@ function readingStats(full, shown) {
6481
7314
  };
6482
7315
  }
6483
7316
  //#endregion
6484
- //#region lib/types/evidence.js
6485
- /**
6486
- * Which session frames are EVIDENCE, and which are bookkeeping.
6487
- *
6488
- * A plan op must cite `evidence` seqs, and the validator's range rule
6489
- * (`evolution-plan-validator`, rule `EVIDENCE_RANGE`) answers only "is this an integer inside
6490
- * the window the plan was authored against". A boundary frame satisfies that while carrying
6491
- * nothing a claim can rest on: the `turn/start` seq is the frame a claim about that turn was
6492
- * most likely read out of, and citing it is indistinguishable from citing a real exchange. This
6493
- * module owns the ONE classification of a session frame by what it CARRIES, so the plan path can
6494
- * report an op whose whole citation list is bookkeeping (design
6495
- * `dsh-evolution-skill-history-design.md` §3D, phase 1: report only — no op is refused here).
6496
- *
6497
- * The classification is a DENY list of the pure bookends, not an allow list of content frames.
6498
- * The platform's own vocabulary (`KNOWN_SESSION_EVENT_TYPES`, 57 members at 0.1.5-rc.2) mixes
6499
- * content frames with state-change frames, and out-of-repo plugins append types the family cannot
6500
- * enumerate; an unknown frame is therefore treated as carrying content, because a report that
6501
- * fires on an unclassifiable frame is a false positive and phase 1 exists to measure how many TRUE
6502
- * ones there are. Tightening — a narrower definition, or an allow list built from the platform's
6503
- * vocabulary — changes this table and nothing else (`evidenceKindIndex` is the only reader).
6504
- * @module @lmzhen/dsh-evolution-core
6505
- */
6506
- /** The frame types that record a boundary and carry no content: the turn and step bookends. */
6507
- const BOOKKEEPING_FRAME_TYPES = new Set([
6508
- "turn/start",
6509
- "turn/end",
6510
- "step/start",
6511
- "step/end"
6512
- ]);
6513
- /**
6514
- * The seqs of the frames that carry content.
6515
- *
6516
- * @param events - the session's frames, oldest first (any iterable; nothing is retained).
6517
- * @returns the substantive seqs, or `undefined` when NO frame carried a usable seq. The two
6518
- * answers are different facts: an empty SET says "every sequenced frame is a boundary", while
6519
- * `undefined` says "this log cannot tell a boundary from a message" (a stub session in a test,
6520
- * a composition whose events carry no seq). A caller that cannot classify must stay silent
6521
- * rather than report every op — the same three-state posture as `sessionReadSkillNames`.
6522
- */
6523
- function evidenceKindIndex(events) {
6524
- const substantive = /* @__PURE__ */ new Set();
6525
- let sequenced = 0;
6526
- for (const event of events) {
6527
- const seq = event.seq;
6528
- if (typeof seq !== "number" || !Number.isInteger(seq)) continue;
6529
- sequenced += 1;
6530
- if (typeof event.type === "string" && BOOKKEEPING_FRAME_TYPES.has(event.type)) continue;
6531
- substantive.add(seq);
6532
- }
6533
- return sequenced === 0 ? void 0 : substantive;
6534
- }
6535
- //#endregion
6536
7317
  //#region lib/types/drift-signals.js
6537
7318
  /**
6538
7319
  * Library-level drift signals for the maintenance subagent (design 011).
@@ -11327,12 +12108,27 @@ const PARAM_NAMESPACES = Object.freeze({
11327
12108
  "tool-skill-manage": "evolution-skills"
11328
12109
  });
11329
12110
  /**
11330
- * The settings namespace one owner package registers. The registry's map is the only
11331
- * place a namespace is spelled: a package without an entry has no user layer, so a
11332
- * missing entry must fail loud here instead of letting a package fall back to a
11333
- * private constant that the registry never sees.
12111
+ * The rows whose settings namespace CHANGED with the platform-line move (G3): the
12112
+ * platform keys a settings section by the Loader entry id now, while 0.1.x let each
12113
+ * plugin register a free-form namespace — this table is the difference, DERIVED from
12114
+ * {@link PARAM_NAMESPACES} so freezing a row id renames it in one place.
12115
+ *
12116
+ * It is the migration's TARGET (`rowId`) and its SOURCE (`namespace`, the section a
12117
+ * stored value sits in). Rows whose two names already agree are absent: there is
12118
+ * nothing to move for them.
12119
+ */
12120
+ const NAMESPACE_MIGRATIONS = Object.freeze(Object.entries(PARAM_NAMESPACES).filter(([owner, namespace]) => namespace !== owner).map(([owner, namespace]) => Object.freeze({
12121
+ namespace,
12122
+ rowId: owner
12123
+ })));
12124
+ /**
12125
+ * The LEGACY settings namespace one owner package used to register through the family's
12126
+ * own seam (0.1.x). It is no longer what the platform calls the section: the platform
12127
+ * derives a settings namespace from the Loader entry id, and this registry's `owner` field
12128
+ * IS that row id, so a live lookup uses {@link paramRowId}. What remains here is the
12129
+ * migration SOURCE (G3 moves a user layer out of this string and into the row id).
11334
12130
  * @param owner - owner package directory name (the map's key).
11335
- * @returns the namespace that owner registers.
12131
+ * @returns the legacy namespace that owner registered.
11336
12132
  * @throws when the owner has no entry in {@link PARAM_NAMESPACES}.
11337
12133
  */
11338
12134
  function paramNamespace(owner) {
@@ -11341,94 +12137,31 @@ function paramNamespace(owner) {
11341
12137
  return namespace;
11342
12138
  }
11343
12139
  /**
11344
- * G3: expose one parameter section to the user layer.
11345
- *
11346
- * Precedence stays 'user > deployment > default': the caller keeps reading its
11347
- * deployment carriers (policy snapshot, then the plugin row) and consults
11348
- * {@link ParamOverrides.get} FIRST — an unset key returns undefined, so the
11349
- * deployment value keeps winning and the family's shadowing rules survive.
11350
- *
11351
- * Failure posture: a provider without `describe` (or one whose describe throws)
11352
- * leaves the user layer UNAVAILABLE and warns once — deployment values then
11353
- * apply. Treating an unreadable user layer as 'no overrides' would silently
11354
- * ignore a setting the user did write, so the warning names it.
11355
- * @typeParam T - the section's value type.
11356
- * @param provider - the platform settings provider, or undefined when absent.
11357
- * @param namespace - namespace to register.
11358
- * @param schema - schemastery schema the platform validates against.
11359
- * @param base - composition base layer (the plugin row's values).
11360
- * @param options - warning sink and the change hook.
11361
- * @returns a reader bound to the namespace.
11362
- */
11363
- function paramSectionOverrides(provider, namespace, schema, base, options = {}) {
11364
- if (provider === void 0) return unavailableOverrides(namespace, base);
11365
- const scope = options.validate === void 0 ? provider.register(namespace, schema, {
11366
- base,
11367
- applies: "live"
11368
- }) : provider.register(namespace, schema, {
11369
- base,
11370
- applies: "live",
11371
- validate: options.validate
11372
- });
11373
- const warn = options.warn ?? (() => {});
11374
- let user = readUserLayer(provider, namespace);
11375
- if (user === void 0) warn("settings provider for " + namespace + " exposes no readable user layer; deployment values apply");
11376
- scope.watch(() => {
11377
- const next = readUserLayer(provider, namespace);
11378
- if (next === void 0) {
11379
- warn("settings provider for " + namespace + " stopped exposing its user layer; deployment values apply");
11380
- user = void 0;
11381
- options.onChange?.();
11382
- return;
11383
- }
11384
- user = next;
11385
- options.onChange?.();
11386
- });
11387
- return {
11388
- namespace,
11389
- get: (key) => user === void 0 ? void 0 : user[key],
11390
- resolved: () => scope.get() ?? base
11391
- };
11392
- }
11393
- /** The pre-provider reader: no user layer, deployment values only. */
11394
- function unavailableOverrides(namespace, base) {
11395
- return {
11396
- namespace,
11397
- get: () => void 0,
11398
- resolved: () => base
11399
- };
11400
- }
11401
- /** Read the raw user section, or undefined when the provider cannot report it. */
11402
- function readUserLayer(provider, namespace) {
11403
- if (provider.describe === void 0) return void 0;
11404
- try {
11405
- return provider.describe({ redactSecrets: false }).find((entry) => entry.ns === namespace)?.user ?? {};
11406
- } catch {
11407
- return;
11408
- }
12140
+ * The settings id one row's user layer lives under, or undefined when the owner publishes
12141
+ * no user layer at all (a deployment-only row: its knobs are E2 and never reach a settings
12142
+ * section). The platform derives a settings namespace from the Loader entry id and reports
12143
+ * it in `settings.describe()` / `configForms.get(entryId)`, and an owner package's row id is
12144
+ * its own name — spelled the same in every bundle patch.
12145
+ *
12146
+ * This is the id to LOOK UP with; {@link paramNamespace} keeps the legacy string the 0.1.x
12147
+ * seam registered, which is the G3 migration source and never a live lookup key.
12148
+ * @param owner - owner package directory name (the row id).
12149
+ * @returns the entry id the platform reports for that row, or undefined for a row without
12150
+ * a user layer.
12151
+ */
12152
+ function paramSettingsId(owner) {
12153
+ return PARAM_NAMESPACES[owner] === void 0 ? void 0 : owner;
11409
12154
  }
11410
12155
  /**
11411
- * Attach a parameter section through the optional settings service.
11412
- * @typeParam T - the section's value type.
11413
- * @param host - the plugin context (structurally typed).
11414
- * @param namespace - namespace to register.
11415
- * @param schema - schemastery schema the platform validates against.
11416
- * @param base - composition base layer (the plugin row's values).
11417
- * @param options - warning sink and the change hook.
11418
- * @returns a reader that follows the provider when it appears.
12156
+ * The settings id of a row that MUST have a user layer (an owner reading its own section).
12157
+ * @param owner - owner package directory name (the row id).
12158
+ * @returns the entry id the platform reports for that row.
12159
+ * @throws when the owner publishes no user layer.
11419
12160
  */
11420
- function installParamSection(host, namespace, schema, base, options = {}) {
11421
- let current = unavailableOverrides(namespace, base);
11422
- host.inject(["settings"], (injected) => {
11423
- const settings = injected.settings;
11424
- current = paramSectionOverrides(settings, namespace, schema, base, options);
11425
- options.onChange?.();
11426
- });
11427
- return {
11428
- namespace,
11429
- get: (key) => current.get(key),
11430
- resolved: () => current.resolved()
11431
- };
12161
+ function paramRowId(owner) {
12162
+ const id = paramSettingsId(owner);
12163
+ if (id === void 0) throw new Error("no settings namespace registered for owner `" + owner + "` (add it to PARAM_NAMESPACES)");
12164
+ return id;
11432
12165
  }
11433
12166
  /**
11434
12167
  * Number-typed read over {@link readParam}: the family's tunables are numbers,
@@ -11443,6 +12176,196 @@ function readNumberParam(carrier, id) {
11443
12176
  return typeof value === "number" ? value : void 0;
11444
12177
  }
11445
12178
  //#endregion
12179
+ //#region lib/types/namespace-migration.js
12180
+ /**
12181
+ * G3: the one-time move of the family's LEGACY settings sections into the row ids the
12182
+ * platform keys settings by now.
12183
+ *
12184
+ * On 0.1.x each plugin registered a free-form namespace (`evolution-memory`, …) through the
12185
+ * family's own seam. The platform derives a settings namespace from the Loader entry id
12186
+ * today, so those old strings address nothing: the platform's own legacy import finds no
12187
+ * such entry, warns, and leaves the values in the renamed document. This module reads that
12188
+ * document, maps each legacy section (and each deprecated key spelling) onto its row, and
12189
+ * merges the values into that row's user layer.
12190
+ *
12191
+ * Two halves, both testable without a host: {@link planNamespaceMigration} is pure (text in,
12192
+ * plan out) and {@link applyNamespaceMigration} takes the few things it needs as
12193
+ * dependencies, so the side-effecting half runs against a fake settings seat.
12194
+ * @module @lmzhen/dsh-evolution-core
12195
+ */
12196
+ /**
12197
+ * Whether a parsed value is a plain mapping (a settings section).
12198
+ * @param value - the parsed value.
12199
+ * @returns true for a non-null, non-array object.
12200
+ */
12201
+ function isSection(value) {
12202
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12203
+ }
12204
+ /**
12205
+ * The key names one row may be written with: its own E3 rows, plus the deprecated
12206
+ * spellings that still resolve to them. An unknown key is refused instead of written —
12207
+ * the platform validates a write against the row's schema, so writing it would fail the
12208
+ * whole row's migration.
12209
+ * @param rowId - the row's entry id.
12210
+ * @returns every key name a legacy section may carry for that row.
12211
+ */
12212
+ function writableKeys(rowId) {
12213
+ const keys = /* @__PURE__ */ new Set();
12214
+ for (const entry of PARAM_EXPOSURE) if (entry.owner === rowId && entry.tier === "E3") keys.add(entry.id);
12215
+ for (const [alias, canonical] of Object.entries(PARAM_ALIASES)) if (keys.has(canonical)) keys.add(alias);
12216
+ return keys;
12217
+ }
12218
+ /**
12219
+ * Read one legacy settings document into a migration plan.
12220
+ *
12221
+ * Pure: the caller reads the file (the platform's renamed `settings.yaml.imported` first,
12222
+ * then a `settings.yaml` the platform has not imported yet) and hands the text over.
12223
+ * @param documentText - the YAML document text.
12224
+ * @returns what to write, what the family knows but need not touch, and what it refused.
12225
+ * @throws {Error} when the document is not a mapping, or a family section is not one.
12226
+ */
12227
+ function planNamespaceMigration(documentText) {
12228
+ const parsed = parse(documentText);
12229
+ if (parsed === void 0 || parsed === null) return {
12230
+ entries: [],
12231
+ empty: [],
12232
+ foreign: [],
12233
+ skipped: []
12234
+ };
12235
+ if (!isSection(parsed)) throw new Error("the settings document is not a mapping; refusing to migrate it");
12236
+ const legacy = new Map(NAMESPACE_MIGRATIONS.map((entry) => [entry.namespace, entry.rowId]));
12237
+ const entries = [];
12238
+ const empty = [];
12239
+ const skipped = [];
12240
+ for (const [namespace, section] of Object.entries(parsed)) {
12241
+ const rowId = legacy.get(namespace);
12242
+ if (rowId === void 0) continue;
12243
+ if (!isSection(section)) throw new Error("settings section `" + namespace + "` is not a mapping; refusing to migrate it");
12244
+ const allowed = writableKeys(rowId);
12245
+ const values = [];
12246
+ for (const [key, value] of Object.entries(section)) {
12247
+ const canonical = resolveParamId(key);
12248
+ if (!allowed.has(canonical)) {
12249
+ skipped.push({
12250
+ namespace,
12251
+ key,
12252
+ reason: "row `" + rowId + "` has no such parameter"
12253
+ });
12254
+ continue;
12255
+ }
12256
+ values.push({
12257
+ source: key,
12258
+ key: canonical,
12259
+ value
12260
+ });
12261
+ }
12262
+ if (values.length === 0) empty.push(namespace);
12263
+ else entries.push({
12264
+ namespace,
12265
+ rowId,
12266
+ values
12267
+ });
12268
+ }
12269
+ return {
12270
+ entries,
12271
+ empty,
12272
+ foreign: Object.keys(parsed).filter((name) => !legacy.has(name)).sort(),
12273
+ skipped
12274
+ };
12275
+ }
12276
+ /**
12277
+ * Whether two JSON-shaped config values are the same value.
12278
+ * @param left - one value.
12279
+ * @param right - the other.
12280
+ * @returns true when both serialize identically.
12281
+ */
12282
+ function sameValue(left, right) {
12283
+ return JSON.stringify(left) === JSON.stringify(right);
12284
+ }
12285
+ /**
12286
+ * Decide what a plan would do: the rows it would write, the keys already equal, the stored
12287
+ * values that win, and the refusals. The write half and the read-only doctor read the SAME
12288
+ * decision here — one rule, two consumers, so a report can never disagree with a run.
12289
+ * @param plan - the plan from {@link planNamespaceMigration}.
12290
+ * @param read - the row's current user layer (see {@link NamespaceMigrationDeps.read}).
12291
+ * @returns the decision; nothing is written.
12292
+ */
12293
+ function classifyNamespaceMigration(plan, read) {
12294
+ const write = [];
12295
+ const conflicts = [];
12296
+ const unchanged = [];
12297
+ const skipped = [...plan.skipped];
12298
+ for (const entry of plan.entries) {
12299
+ const view = read(entry.rowId);
12300
+ if (view === void 0) {
12301
+ for (const value of entry.values) skipped.push({
12302
+ namespace: entry.namespace,
12303
+ key: value.source,
12304
+ reason: "row `" + entry.rowId + "` is not served in this composition"
12305
+ });
12306
+ continue;
12307
+ }
12308
+ const user = view.user ?? {};
12309
+ const patch = {};
12310
+ for (const value of entry.values) {
12311
+ if (!Object.hasOwn(user, value.key)) {
12312
+ patch[value.key] = value.value;
12313
+ continue;
12314
+ }
12315
+ if (sameValue(user[value.key], value.value)) {
12316
+ unchanged.push({
12317
+ rowId: entry.rowId,
12318
+ key: value.key
12319
+ });
12320
+ continue;
12321
+ }
12322
+ conflicts.push({
12323
+ rowId: entry.rowId,
12324
+ key: value.key,
12325
+ current: user[value.key],
12326
+ legacy: value.value
12327
+ });
12328
+ }
12329
+ if (Object.keys(patch).length > 0) write.push({
12330
+ rowId: entry.rowId,
12331
+ patch
12332
+ });
12333
+ }
12334
+ return {
12335
+ write,
12336
+ conflicts,
12337
+ unchanged,
12338
+ skipped
12339
+ };
12340
+ }
12341
+ /**
12342
+ * Report one kept value: the stored value is the newer decision, so it wins and the legacy
12343
+ * one is named rather than dropped.
12344
+ * @param conflict - the conflict the classification found.
12345
+ * @returns the warning text.
12346
+ */
12347
+ function conflictWarning(conflict) {
12348
+ return "evolution-migration: row `" + conflict.rowId + "` already sets `" + conflict.key + "`; the stored value was kept and the legacy " + JSON.stringify(conflict.legacy) + " was not applied";
12349
+ }
12350
+ async function applyNamespaceMigration(plan, deps) {
12351
+ const decided = classifyNamespaceMigration(plan, (rowId) => deps.read(rowId));
12352
+ for (const conflict of decided.conflicts) deps.warn(conflictWarning(conflict));
12353
+ const written = [];
12354
+ for (const row of decided.write) {
12355
+ await deps.update(row.rowId, row.patch);
12356
+ written.push({
12357
+ rowId: row.rowId,
12358
+ keys: Object.keys(row.patch)
12359
+ });
12360
+ }
12361
+ return {
12362
+ written,
12363
+ conflicts: decided.conflicts,
12364
+ unchanged: decided.unchanged,
12365
+ skipped: decided.skipped
12366
+ };
12367
+ }
12368
+ //#endregion
11446
12369
  //#region lib/types/opt-in.js
11447
12370
  /**
11448
12371
  * Tool names only a session that mounted the family's MODEL rows can see.
@@ -11539,4 +12462,4 @@ function sessionAudited(ctx, sessionId, sessionScoped) {
11539
12462
  return false;
11540
12463
  }
11541
12464
  //#endregion
11542
- export { ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, describeReferenceRewrite, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, installParamSection, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramSectionOverrides, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, parseLockBody, partitionVersions, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, planReferenceRewrite, planRehoming, policyStageLimits, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionReadSkillNames, sessionSeesFamilyTools, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, yamlPlainScalarNeedsQuotes };
12465
+ export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, describeReferenceRewrite, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, parseLockBody, partitionVersions, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, yamlPlainScalarNeedsQuotes };