@lmzhen/dsh-evolution-core 0.15.2 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lmzhen/dsh-evolution-core
2
2
 
3
- Shared pure library for the family: `SkillLibrary`/`MemoryStore`, the prompts and guidance text, the threat/quality/drift signals, the skill CONTENT history (`skill-history.ts`: content-addressed versions behind every write) and the frame classification the plan path reports over (`evidence.ts`), plus the IO primitives (`nodeEvolutionIo`, `transactIo`, `evolutionIoAdapter`). It is **not a Cordis row** — it registers no service, tool or prompt
3
+ Shared pure library for the family: `SkillLibrary`/`MemoryStore`, the prompts and guidance text, the threat/quality/drift signals, the skill CONTENT history (`skill-history.ts`: content-addressed versions behind every write) and the frame classification the plan path reports over (`evidence.ts`), plus the IO primitives (`nodeEvolutionIo`, `transactIo`, `evolutionIoAdapter`) and the two helpers every host route surface shares (`http-routes.ts`: the loopback fence, the bounded JSON body reader and the JSON writer). It is **not a Cordis row** — it registers no service, tool or prompt
4
4
  section, and importing the package root is its only entry.
5
5
 
6
6
  ## Model surface
@@ -30,6 +30,14 @@ section, and importing the package root is its only entry.
30
30
  (`contentRetentionFeedback`, design §4 item 3): the ratio, both character counts, and the fact that the
31
31
  replaced version is still in the history. Computed in the write funnel where both bodies are in hand, feedback
32
32
  only — no ratio refuses a write, and the function owns no configuration.
33
+ - **A REMOVAL now leaves an entry (E2, 2026-09-30).** `nextHistoryIndex` mints an entry from the AFTER side,
34
+ so an operation with `after === null` (removing a support file) used to record nothing once its bytes were
35
+ already indexed: the deletion was invisible and its action vocabulary unreachable. A removal records the bytes
36
+ it removed, labeled with the write that removed them, so a face can say "these bytes are gone" and undoing that
37
+ entry puts the file back. The entry deliberately carries no `beforeHash` (its own hash IS the removed content).
38
+ - `anchorVerdict` is exported: the stage-time anchor rule ("does this write's target still hold the bytes the
39
+ caller staged against?") has ONE implementation, and read-only readers (a preview, a dry run) call it instead
40
+ of re-deriving a `contentHash` comparison.
33
41
  - Content history is HISTORY ONLY: the skill tree is the single truth for current content, and no judgement reads the version index (design invariant I1). Recording is best-effort but happens inside the write lock, so the index never describes content that did not land; a failed record warns once per instance instead of failing the write.
34
42
  - Skill-library mutations are read-modify-write on one file, so `SkillLibrary` serializes them in-process with a `makeSerialQueue` chain: `update`, `patch`, `restructure`, `writeSupportFile` and (since 0.3.46) `consolidate`'s target read→merge→commit run their whole read→validate→write under one serial task, so two concurrent mutators on one skill never interleave in this process.
35
43
  - `create` is INSIDE the serial chain and, when a transact backend is bound, its exists check runs inside the same per-file transact (v18); `archive`/`consolidate` are rename-based two-phase paths and stay outside the single-file serial chain.
package/lib/index.js CHANGED
@@ -2954,6 +2954,207 @@ async function readEvolutionTimeline(io, path, archives) {
2954
2954
  };
2955
2955
  }
2956
2956
  //#endregion
2957
+ //#region lib/types/fuzzy-match.js
2958
+ /**
2959
+ * Fuzzy string matching and replacement for patch/restructure edits of skill files.
2960
+ *
2961
+ * Split out of skill-store.ts (S2-1): pure text functions with no store state.
2962
+ * The store imports the scan, the budgets and the replace entry points directly;
2963
+ * none of them is re-exported, so the package export surface is unchanged.
2964
+ */
2965
+ function fuzzyIndexOf(content, pattern, from = 0) {
2966
+ const isSpace = (char) => char !== void 0 && /[ \t]/.test(char);
2967
+ const escaped = (char) => {
2968
+ if (char === "n") return "\n";
2969
+ if (char === "t") return " ";
2970
+ if (char === "r") return "\r";
2971
+ return null;
2972
+ };
2973
+ for (let start = from; start < content.length; start += 1) {
2974
+ let contentIndex = start;
2975
+ let patternIndex = 0;
2976
+ while (patternIndex < pattern.length && contentIndex < content.length) {
2977
+ const patternChar = pattern[patternIndex];
2978
+ const contentChar = content[contentIndex];
2979
+ if (isSpace(patternChar)) {
2980
+ while (patternIndex < pattern.length && isSpace(pattern[patternIndex])) patternIndex += 1;
2981
+ while (contentIndex < content.length && isSpace(content[contentIndex])) contentIndex += 1;
2982
+ continue;
2983
+ }
2984
+ const escapedChar = patternChar === "\\" ? escaped(pattern[patternIndex + 1]) : null;
2985
+ if (escapedChar !== null && contentChar === escapedChar) {
2986
+ patternIndex += 2;
2987
+ contentIndex += 1;
2988
+ continue;
2989
+ }
2990
+ if (patternChar === contentChar) {
2991
+ contentIndex += 1;
2992
+ patternIndex += 1;
2993
+ continue;
2994
+ }
2995
+ break;
2996
+ }
2997
+ if (patternIndex === pattern.length) return [start, contentIndex];
2998
+ }
2999
+ return null;
3000
+ }
3001
+ /** V6-17 (0.3.37): the fuzzy-patch scan is O(n·m) with no input bound; a
3002
+ * non-exact anchor past these budgets would block the event loop (measured
3003
+ * ~6s at 20k×20k). Exact matches go through the fast `includes` path and stay
3004
+ * allowed regardless of size. */
3005
+ const FUZZY_MAX_PATTERN_CHARS = 4096;
3006
+ const FUZZY_MAX_WORK = 8e6;
3007
+ /** Trim leading whitespace of the first line and trailing whitespace of the last line. */
3008
+ function trimPatternBoundaries(pattern) {
3009
+ const from = pattern.search(/\S/);
3010
+ const trimmed = from < 0 ? pattern : pattern.slice(from);
3011
+ const trailing = trimmed.search(/\s+$/);
3012
+ return trailing < 0 ? trimmed : trimmed.slice(0, trailing);
3013
+ }
3014
+ /** Replace only the fuzzy-matched span, preserving all surrounding bytes.
3015
+ * V7-11 (0.3.44): the replaceAll loop accumulates the per-scan cost — the
3016
+ * single-scan budget at the caller only bounded ONE fuzzyIndexOf, while an
3017
+ * unbounded number of matches × O(n·m) each could still stall the loop.
3018
+ * When the accumulated cost exceeds the budget the whole replace fails
3019
+ * (null) instead of partially applying an arbitrary prefix. */
3020
+ function fuzzyReplace(content, oldString, newString, replaceAll) {
3021
+ let current = content;
3022
+ let scanFrom = 0;
3023
+ let totalWork = 0;
3024
+ for (;;) {
3025
+ totalWork += current.length * oldString.length;
3026
+ if (totalWork > 8e6) return null;
3027
+ const match = fuzzyIndexOf(current, oldString, scanFrom);
3028
+ if (match === null) return current;
3029
+ const [start, end] = match;
3030
+ const next = current.slice(0, start) + newString + current.slice(end);
3031
+ if (!replaceAll) return next;
3032
+ current = next;
3033
+ scanFrom = start + newString.length;
3034
+ }
3035
+ }
3036
+ function fuzzyPatch(content, oldString, newString, replaceAll = false) {
3037
+ if (oldString === "") return null;
3038
+ if (content.includes(oldString)) return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
3039
+ const boundary = trimPatternBoundaries(oldString);
3040
+ if (boundary === "") return null;
3041
+ if (boundary !== oldString) {
3042
+ if (fuzzyIndexOf(content, boundary) !== null) return fuzzyReplace(content, boundary, newString, replaceAll);
3043
+ }
3044
+ if (fuzzyIndexOf(content, oldString) !== null) return fuzzyReplace(content, oldString, newString, replaceAll);
3045
+ return null;
3046
+ }
3047
+ /** Deterministic section-extraction plan facts; the caller owns the IO and the append semantics. */
3048
+ //#endregion
3049
+ //#region lib/types/http-routes.js
3050
+ /**
3051
+ * The loopback HTTP plumbing every family route surface shares (E-302-era conventions).
3052
+ *
3053
+ * WHY THIS IS IN CORE: the fence, the bounded body reader and the JSON writer are the same for every
3054
+ * family route surface, and `evolution-core` is the one package all of them already depend on. The
3055
+ * canonical platform implementation (packages/client/connection/src/api-request-trust.ts) is not part
3056
+ * of that package's published surface, so the family keeps its own copy of the RULES here — once.
3057
+ *
3058
+ * Layering: pure transport. Nothing here knows about skills, approvals, curators or route tables.
3059
+ * @module @lmzhen/dsh-evolution-core/http-routes
3060
+ */
3061
+ /** Largest request body a family route accepts; a bigger one reads as "no body" (400 from the handler). */
3062
+ const MAX_REQUEST_BODY_BYTES = 64 * 1024;
3063
+ /** One header as a single string (a repeated header reads as absent rather than as its first value). */
3064
+ function header(request, name) {
3065
+ const value = request.headers[name];
3066
+ return typeof value === "string" ? value : void 0;
3067
+ }
3068
+ /** IPv4 127/8 predicate (four decimal octets, first == 127). */
3069
+ function isIPv4Loopback(value) {
3070
+ const parts = value.split(".");
3071
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
3072
+ }
3073
+ /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
3074
+ function isLoopbackAddress(address) {
3075
+ if (address === void 0) return false;
3076
+ const normalized = address.toLowerCase();
3077
+ if (normalized === "::1") return true;
3078
+ if (normalized.startsWith("::ffff:")) return isIPv4Loopback(normalized.slice(7));
3079
+ return isIPv4Loopback(normalized);
3080
+ }
3081
+ /** Whether a hostname names the loopback authority (localhost, [::1], 127/8). */
3082
+ function isLoopbackHostname(hostname) {
3083
+ if (hostname === "localhost" || hostname === "[::1]") return true;
3084
+ return isIPv4Loopback(hostname);
3085
+ }
3086
+ /**
3087
+ * Whether one request may enter a family route.
3088
+ *
3089
+ * Rules, in order: the SOCKET must be loopback (authoritative — X-Forwarded-For is never trusted), the
3090
+ * Host header must name a loopback authority (DNS-rebinding defense), an explicit cross-site marker is
3091
+ * refused, and an attached Origin must be exactly this authority.
3092
+ * @param request - the request to judge.
3093
+ * @returns true when the request may proceed.
3094
+ */
3095
+ function isLoopbackRequest(request) {
3096
+ if (!isLoopbackAddress(request.socket?.remoteAddress)) return false;
3097
+ const host = header(request, "host");
3098
+ if (host === void 0) return false;
3099
+ let hostUrl;
3100
+ try {
3101
+ hostUrl = new URL("http://" + host);
3102
+ } catch {
3103
+ return false;
3104
+ }
3105
+ if (!isLoopbackHostname(hostUrl.hostname)) return false;
3106
+ if (header(request, "sec-fetch-site") === "cross-site") return false;
3107
+ const origin = header(request, "origin");
3108
+ if (origin === void 0) return true;
3109
+ try {
3110
+ return new URL(origin).host === hostUrl.host;
3111
+ } catch {
3112
+ return false;
3113
+ }
3114
+ }
3115
+ /** Write one JSON response. The routes own their response lifecycle, so every path ends here. */
3116
+ function writeJson(res, status, value) {
3117
+ const body = JSON.stringify(value);
3118
+ res.writeHead(status, {
3119
+ "content-type": "application/json; charset=utf-8",
3120
+ "content-length": Buffer.byteLength(body)
3121
+ });
3122
+ res.end(body);
3123
+ }
3124
+ /**
3125
+ * Read one JSON request body, bounded.
3126
+ * @param req - the request.
3127
+ * @returns the parsed value, or null when the body is oversized, empty, or not JSON.
3128
+ */
3129
+ async function readJsonBody(req) {
3130
+ const chunks = [];
3131
+ let size = 0;
3132
+ for await (const chunk of req) {
3133
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3134
+ size += buffer.byteLength;
3135
+ if (size > 65536) return null;
3136
+ chunks.push(buffer);
3137
+ }
3138
+ if (size === 0) return null;
3139
+ try {
3140
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
3141
+ } catch {
3142
+ return null;
3143
+ }
3144
+ }
3145
+ /**
3146
+ * Read one JSON request body that must be an OBJECT.
3147
+ *
3148
+ * The three route surfaces all take named fields, so "array or scalar" is a bad request rather than a
3149
+ * shape each handler has to re-check.
3150
+ * @param req - the request.
3151
+ * @returns the body as a plain object, or null when it is absent, oversized, not JSON, or not an object.
3152
+ */
3153
+ async function readJsonObject(req) {
3154
+ const body = await readJsonBody(req);
3155
+ return body !== null && typeof body === "object" && !Array.isArray(body) ? body : null;
3156
+ }
3157
+ //#endregion
2957
3158
  //#region lib/types/env.js
2958
3159
  /**
2959
3160
  * Evolution environment-variable reading, single-source (WC, 0.3.56).
@@ -6864,6 +7065,7 @@ function nextHistoryIndex(versions, input, keep) {
6864
7065
  chars: input.before?.length ?? 0,
6865
7066
  ...pathField
6866
7067
  });
7068
+ const removalHash = afterHash === null ? beforeHash : null;
6867
7069
  if (afterHash !== null) {
6868
7070
  if (grown[grown.length - 1]?.hash !== afterHash) grown.push({
6869
7071
  v: nextVersion(grown),
@@ -6876,6 +7078,18 @@ function nextHistoryIndex(versions, input, keep) {
6876
7078
  ...beforeHash === null ? {} : { beforeHash }
6877
7079
  });
6878
7080
  }
7081
+ if (removalHash !== null) {
7082
+ const tail = grown[grown.length - 1];
7083
+ if (tail?.hash !== removalHash || tail.action !== input.action) grown.push({
7084
+ v: nextVersion(grown),
7085
+ at: input.at,
7086
+ action: input.action,
7087
+ hash: removalHash,
7088
+ chars: input.before?.length ?? 0,
7089
+ ...pathField,
7090
+ ...input.summary === void 0 ? {} : { summary: input.summary }
7091
+ });
7092
+ }
6879
7093
  const trimmed = grown.slice(Math.max(0, grown.length - clampKeep(keep)));
6880
7094
  const recorded = {};
6881
7095
  if (beforeHash !== null) {
@@ -7145,92 +7359,6 @@ function elapsedSince(at, nowMs) {
7145
7359
  };
7146
7360
  }
7147
7361
  //#endregion
7148
- //#region lib/types/fuzzy-match.js
7149
- /**
7150
- * Fuzzy string matching and replacement for patch/restructure edits of skill files.
7151
- *
7152
- * Split out of skill-store.ts (S2-1): pure text functions with no store state.
7153
- * The store imports the scan, the budgets and the replace entry points directly;
7154
- * none of them is re-exported, so the package export surface is unchanged.
7155
- */
7156
- function fuzzyIndexOf(content, pattern, from = 0) {
7157
- const isSpace = (char) => char !== void 0 && /[ \t]/.test(char);
7158
- const escaped = (char) => {
7159
- if (char === "n") return "\n";
7160
- if (char === "t") return " ";
7161
- if (char === "r") return "\r";
7162
- return null;
7163
- };
7164
- for (let start = from; start < content.length; start += 1) {
7165
- let contentIndex = start;
7166
- let patternIndex = 0;
7167
- while (patternIndex < pattern.length && contentIndex < content.length) {
7168
- const patternChar = pattern[patternIndex];
7169
- const contentChar = content[contentIndex];
7170
- if (isSpace(patternChar)) {
7171
- while (patternIndex < pattern.length && isSpace(pattern[patternIndex])) patternIndex += 1;
7172
- while (contentIndex < content.length && isSpace(content[contentIndex])) contentIndex += 1;
7173
- continue;
7174
- }
7175
- const escapedChar = patternChar === "\\" ? escaped(pattern[patternIndex + 1]) : null;
7176
- if (escapedChar !== null && contentChar === escapedChar) {
7177
- patternIndex += 2;
7178
- contentIndex += 1;
7179
- continue;
7180
- }
7181
- if (patternChar === contentChar) {
7182
- contentIndex += 1;
7183
- patternIndex += 1;
7184
- continue;
7185
- }
7186
- break;
7187
- }
7188
- if (patternIndex === pattern.length) return [start, contentIndex];
7189
- }
7190
- return null;
7191
- }
7192
- /** Trim leading whitespace of the first line and trailing whitespace of the last line. */
7193
- function trimPatternBoundaries(pattern) {
7194
- const from = pattern.search(/\S/);
7195
- const trimmed = from < 0 ? pattern : pattern.slice(from);
7196
- const trailing = trimmed.search(/\s+$/);
7197
- return trailing < 0 ? trimmed : trimmed.slice(0, trailing);
7198
- }
7199
- /** Replace only the fuzzy-matched span, preserving all surrounding bytes.
7200
- * V7-11 (0.3.44): the replaceAll loop accumulates the per-scan cost — the
7201
- * single-scan budget at the caller only bounded ONE fuzzyIndexOf, while an
7202
- * unbounded number of matches × O(n·m) each could still stall the loop.
7203
- * When the accumulated cost exceeds the budget the whole replace fails
7204
- * (null) instead of partially applying an arbitrary prefix. */
7205
- function fuzzyReplace(content, oldString, newString, replaceAll) {
7206
- let current = content;
7207
- let scanFrom = 0;
7208
- let totalWork = 0;
7209
- for (;;) {
7210
- totalWork += current.length * oldString.length;
7211
- if (totalWork > 8e6) return null;
7212
- const match = fuzzyIndexOf(current, oldString, scanFrom);
7213
- if (match === null) return current;
7214
- const [start, end] = match;
7215
- const next = current.slice(0, start) + newString + current.slice(end);
7216
- if (!replaceAll) return next;
7217
- current = next;
7218
- scanFrom = start + newString.length;
7219
- }
7220
- }
7221
- function fuzzyPatch(content, oldString, newString, replaceAll = false) {
7222
- if (oldString === "") return null;
7223
- if (content.includes(oldString)) return replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, () => newString);
7224
- const boundary = trimPatternBoundaries(oldString);
7225
- if (boundary === "") return null;
7226
- if (boundary !== oldString) {
7227
- if (fuzzyIndexOf(content, boundary) !== null) return fuzzyReplace(content, boundary, newString, replaceAll);
7228
- }
7229
- if (fuzzyIndexOf(content, oldString) !== null) return fuzzyReplace(content, oldString, newString, replaceAll);
7230
- return null;
7231
- }
7232
- /** Deterministic section-extraction plan facts; the caller owns the IO and the append semantics. */
7233
- //#endregion
7234
7362
  //#region lib/types/skill-store.js
7235
7363
  /**
7236
7364
  * Skill library management for the self-evolution plugin.
@@ -11411,4 +11539,4 @@ function sessionAudited(ctx, sessionId, sessionScoped) {
11411
11539
  return false;
11412
11540
  }
11413
11541
  //#endregion
11414
- 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, 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_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, 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, getRecord, historyIndexFile, historyRoot, identity, idleDays, installParamSection, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, 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, 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, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, yamlPlainScalarNeedsQuotes };
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 };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The loopback HTTP plumbing every family route surface shares (E-302-era conventions).
3
+ *
4
+ * WHY THIS IS IN CORE: the fence, the bounded body reader and the JSON writer are the same for every
5
+ * family route surface, and `evolution-core` is the one package all of them already depend on. The
6
+ * canonical platform implementation (packages/client/connection/src/api-request-trust.ts) is not part
7
+ * of that package's published surface, so the family keeps its own copy of the RULES here — once.
8
+ *
9
+ * Layering: pure transport. Nothing here knows about skills, approvals, curators or route tables.
10
+ * @module @lmzhen/dsh-evolution-core/http-routes
11
+ */
12
+ import type { IncomingMessage, ServerResponse } from 'node:http';
13
+ /** Largest request body a family route accepts; a bigger one reads as "no body" (400 from the handler). */
14
+ export declare const MAX_REQUEST_BODY_BYTES: number;
15
+ /** The request facts the trust fence reads: a Node request, or the same fields in a spec fixture. */
16
+ export interface FenceRequest {
17
+ readonly headers: Record<string, string | string[] | undefined>;
18
+ readonly socket?: {
19
+ readonly remoteAddress?: string | undefined;
20
+ } | undefined;
21
+ }
22
+ /**
23
+ * Whether one request may enter a family route.
24
+ *
25
+ * Rules, in order: the SOCKET must be loopback (authoritative — X-Forwarded-For is never trusted), the
26
+ * Host header must name a loopback authority (DNS-rebinding defense), an explicit cross-site marker is
27
+ * refused, and an attached Origin must be exactly this authority.
28
+ * @param request - the request to judge.
29
+ * @returns true when the request may proceed.
30
+ */
31
+ export declare function isLoopbackRequest(request: FenceRequest): boolean;
32
+ /** Write one JSON response. The routes own their response lifecycle, so every path ends here. */
33
+ export declare function writeJson(res: ServerResponse, status: number, value: unknown): void;
34
+ /**
35
+ * Read one JSON request body, bounded.
36
+ * @param req - the request.
37
+ * @returns the parsed value, or null when the body is oversized, empty, or not JSON.
38
+ */
39
+ export declare function readJsonBody(req: IncomingMessage): Promise<unknown>;
40
+ /**
41
+ * Read one JSON request body that must be an OBJECT.
42
+ *
43
+ * The three route surfaces all take named fields, so "array or scalar" is a bad request rather than a
44
+ * shape each handler has to re-check.
45
+ * @param req - the request.
46
+ * @returns the body as a plain object, or null when it is absent, oversized, not JSON, or not an object.
47
+ */
48
+ export declare function readJsonObject(req: IncomingMessage): Promise<Record<string, unknown> | null>;
49
+ //# sourceMappingURL=http-routes.d.ts.map
@@ -32,7 +32,9 @@ export * from './reference-rewrite.ts';
32
32
  export * from './cost.ts';
33
33
  export * from './curator.ts';
34
34
  export * from './evolution-events.ts';
35
+ export * from './fuzzy-match.ts';
35
36
  export * from './gates.ts';
37
+ export * from './http-routes.ts';
36
38
  export * from './events.ts';
37
39
  export * from './env.ts';
38
40
  export * from './errors.ts';
@@ -84,6 +84,13 @@ export type WriteAnchor = {
84
84
  * - `missing`: the target does not exist (an `absent` anchor `match`es this).
85
85
  */
86
86
  export type AnchorVerdict = 'match' | 'drift' | 'missing';
87
+ /**
88
+ * Evaluate a stage-time anchor against the bytes a locked read observed.
89
+ * @param anchor - the caller's anchor, or `undefined` for an unanchored write.
90
+ * @param current - the bytes the write lock read (`null` = the target is absent).
91
+ * @returns `match` when the write may proceed, otherwise the refusal verdict.
92
+ */
93
+ export declare function anchorVerdict(anchor: WriteAnchor | undefined, current: string | null): AnchorVerdict;
87
94
  export interface SkillActionResult {
88
95
  ok: boolean;
89
96
  message: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.15.2",
4
+ "version": "0.16.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },