@lmzhen/dsh-evolution-core 0.30.31 → 0.30.32

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
@@ -4,9 +4,9 @@ import { w as allowRowCollisions } from "./composition-DSesmTvf.js";
4
4
  import { n as errorText, t as EVOLUTION_ERRORS } from "./errors-Br2-uUSK.js";
5
5
  import { createSubagentChannel, probeChannelState, subagentChannelState } from "./subagent/index.js";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
- import { basename, dirname, join, resolve } from "node:path";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
8
8
  import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
9
- import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
9
+ import { cp, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { parse } from "yaml";
12
12
  import { homedir } from "node:os";
@@ -2251,6 +2251,40 @@ function describeReferenceRewrite(plan) {
2251
2251
  ].filter((part) => part.length > 0).join("; ")}`;
2252
2252
  }
2253
2253
  //#endregion
2254
+ //#region lib/types/support-refs.js
2255
+ /**
2256
+ * Support-directory references in a markdown body (009 kernel): references/…,
2257
+ * templates/…, scripts/…, assets/… relative links — any extension and nested
2258
+ * paths. Pure: who checks them and what the verdict is belongs to the caller.
2259
+ * `..` traversal is a different defect class (path validation owns it) and is
2260
+ * not a support link.
2261
+ */
2262
+ function supportRefs(content) {
2263
+ const refs = [];
2264
+ for (const match of content.matchAll(/(?<![A-Za-z0-9._/-])(?:references|templates|scripts|assets)\/[A-Za-z0-9._/-]+/g)) {
2265
+ const ref = match[0];
2266
+ if (ref.includes("..")) continue;
2267
+ refs.push(ref);
2268
+ }
2269
+ return refs;
2270
+ }
2271
+ /**
2272
+ * References that leave the skill root: parent traversals or absolute support paths.
2273
+ * Extract must not silently treat these as relative support files; the caller reports
2274
+ * them (bounded external copy is a separate, explicit policy).
2275
+ * @param content - markdown body.
2276
+ * @returns external-looking support references, de-duplicated in first-seen order.
2277
+ */
2278
+ function externalRefs(content) {
2279
+ const refs = [];
2280
+ for (const match of content.matchAll(/(?:^|[\s("'`])((?:\.\.\/)+[A-Za-z0-9._/-]+|\/[A-Za-z0-9._/-]+)/g)) {
2281
+ const ref = match[1]?.replace(/[.,;:]+$/, "");
2282
+ if (ref === void 0 || ref.length === 0 || refs.includes(ref)) continue;
2283
+ refs.push(ref);
2284
+ }
2285
+ return refs;
2286
+ }
2287
+ //#endregion
2254
2288
  //#region lib/types/constants.js
2255
2289
  /**
2256
2290
  * Shared constants for the dsh-evolution plugin family.
@@ -3047,6 +3081,786 @@ function authoringFeedback(frontmatter) {
3047
3081
  * v28 G2.1 (EVO-IO-05): this is a delegation to the seam's own
3048
3082
  * `isCommittedWarning` — the marker predicate has exactly one definition. */
3049
3083
  //#endregion
3084
+ //#region lib/types/document-body.js
3085
+ /**
3086
+ * What the panel shows when it renders one version as a document.
3087
+ *
3088
+ * Two decisions, both made here so the preview and the rendered diff cannot drift apart:
3089
+ *
3090
+ * - FRONTMATTER IS NOT DOCUMENT TEXT. A SKILL.md opens with a YAML block; the renderer read its
3091
+ * closing `---` as a setext underline, so the metadata line became the document's biggest heading
3092
+ * and the real title sat below it (measured on the installed panel 2026-09-28). The block is
3093
+ * stripped for DISPLAY only: the raw view and the body route still carry every byte, and the
3094
+ * panel already shows the name and description on the row and in the left column.
3095
+ * - STRIPPING FAILS OPEN. Only a block that the family's own reader accepts is removed. A document
3096
+ * whose text merely starts with a thematic break, a block with no closing line, or a frontmatter
3097
+ * whose body is empty is returned UNCHANGED — showing a document's first line is always better
3098
+ * than showing nothing.
3099
+ *
3100
+ * `readingStats` exists for the other half of the reader's honesty: a preview that stops early has
3101
+ * to be able to say HOW much it is not showing.
3102
+ * @module @lmzhen/dsh-evolution-core/document-body
3103
+ */
3104
+ /**
3105
+ * The document a reader sees: the body without its leading frontmatter block.
3106
+ * @param content - the stored bytes of one version.
3107
+ * @returns the text to render; unchanged when there is no frontmatter the reader accepts.
3108
+ */
3109
+ function displayBodyOf(content) {
3110
+ const block = frontmatterBlock(content);
3111
+ if (block === null) return content;
3112
+ if (parseFrontmatter(content) === null) return content;
3113
+ return block.lines.slice(block.end + 1).join("\n").replace(/^\s*\n/, "");
3114
+ }
3115
+ /**
3116
+ * How much of a version the panel is showing.
3117
+ * @param full - the whole stored text.
3118
+ * @param shown - the part the route handed over.
3119
+ * @returns the counts, so the panel can say what it is not showing.
3120
+ */
3121
+ function readingStats(full, shown) {
3122
+ return {
3123
+ totalChars: full.length,
3124
+ shownChars: shown.length,
3125
+ truncated: shown.length < full.length
3126
+ };
3127
+ }
3128
+ /**
3129
+ * The H2 sections of a body, in document order — the ONE scan (G3).
3130
+ *
3131
+ * Both the restructure candidate list and the store anchor validation call this function, so a
3132
+ * candidate can only name a heading that exists EXACTLY ONCE and has content: the two rejections a
3133
+ * hand-written anchor used to hit ("no such heading", "ambiguous anchor") stop being reachable.
3134
+ * H2 is the section boundary (a section spans to the next H2 or EOF), matching the store.
3135
+ * @param body - the markdown after the frontmatter block.
3136
+ * @returns one row per H2 heading, with `count` filled in a second pass.
3137
+ */
3138
+ function h2Sections(body) {
3139
+ const lines = body.split(String.fromCharCode(10));
3140
+ const rows = [];
3141
+ for (let i = 0; i < lines.length; i += 1) {
3142
+ const match = /^#{2}\s+(.+?)\s*$/.exec(lines[i] ?? "");
3143
+ if (match === null) continue;
3144
+ const title = (match[1] ?? "").trim();
3145
+ let end = lines.length;
3146
+ for (let j = i + 1; j < lines.length; j += 1) if (/^#{2}\s/.test(lines[j] ?? "")) {
3147
+ end = j;
3148
+ break;
3149
+ }
3150
+ rows.push({
3151
+ title,
3152
+ start: i,
3153
+ end,
3154
+ count: 0
3155
+ });
3156
+ }
3157
+ for (const row of rows) row.count = rows.filter((other) => other.title === row.title).length;
3158
+ return rows;
3159
+ }
3160
+ /**
3161
+ * The sections a restructure proposal may name: heading unique, section non-empty. Derived from
3162
+ * `h2Sections`, i.e. from the SAME scan the store validates against.
3163
+ * @param body - the markdown after the frontmatter block.
3164
+ * @returns move-ready pairs in document order; empty when nothing qualifies.
3165
+ */
3166
+ function restructureCandidates(body) {
3167
+ return h2Sections(body).filter((section) => section.count === 1 && section.end > section.start + 1).map((section) => ({
3168
+ heading: section.title,
3169
+ toFile: "references/" + section.title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-+|-+$/g, "") + ".md"
3170
+ }));
3171
+ }
3172
+ //#endregion
3173
+ //#region lib/types/section-selection.js
3174
+ const LF$1 = String.fromCharCode(10);
3175
+ /**
3176
+ * Select whole H2 sections or exact content sub-blocks from one body.
3177
+ *
3178
+ * The H2 scan is document-body's h2Sections, so restructure and extract share
3179
+ * one boundary rule. A start/end pair selects an inclusive, contiguous run of
3180
+ * exact lines inside that heading; it cannot select the heading itself, overlap
3181
+ * another request, or leave a selected range empty.
3182
+ * @param body - markdown after frontmatter.
3183
+ * @param requests - exact heading plus optional start/end line pairs.
3184
+ * @returns selected ranges in request order, or the first refusal.
3185
+ */
3186
+ function selectSections(body, requests) {
3187
+ const lines = body.split(LF$1);
3188
+ const sections = h2Sections(body);
3189
+ const selected = [];
3190
+ for (let requestIndex = 0; requestIndex < requests.length; requestIndex += 1) {
3191
+ const request = requests[requestIndex];
3192
+ if (request === void 0) continue;
3193
+ const wanted = request.heading.trim();
3194
+ const matching = sections.filter((section) => section.title === wanted);
3195
+ if (matching.length === 0) return {
3196
+ ok: false,
3197
+ code: "heading-missing",
3198
+ message: "no \"## " + wanted + "\" heading in the body"
3199
+ };
3200
+ if (matching.length > 1) return {
3201
+ ok: false,
3202
+ code: "heading-ambiguous",
3203
+ message: "heading \"## " + wanted + "\" appears " + matching.length + " times (ambiguous anchor)"
3204
+ };
3205
+ const section = matching[0];
3206
+ if (section === void 0) return {
3207
+ ok: false,
3208
+ code: "heading-missing",
3209
+ message: "no \"## " + wanted + "\" heading in the body"
3210
+ };
3211
+ if (selected.some((entry) => entry.start === section.start)) return {
3212
+ ok: false,
3213
+ code: "heading-duplicate",
3214
+ message: "heading \"## " + wanted + "\" is moved twice"
3215
+ };
3216
+ if (!(request.start !== void 0 || request.end !== void 0)) {
3217
+ if (section.end === section.start + 1) return {
3218
+ ok: false,
3219
+ code: "heading-empty",
3220
+ message: "heading \"## " + wanted + "\" has an empty section"
3221
+ };
3222
+ selected.push({
3223
+ heading: wanted,
3224
+ start: section.start,
3225
+ end: section.end,
3226
+ text: lines.slice(section.start, section.end).join(LF$1),
3227
+ requestIndex
3228
+ });
3229
+ continue;
3230
+ }
3231
+ if (request.start === void 0 || request.end === void 0) return {
3232
+ ok: false,
3233
+ code: "part-anchor-incomplete",
3234
+ message: "heading \"## " + wanted + "\" needs both start and end anchors"
3235
+ };
3236
+ const startMatches = [];
3237
+ const endMatches = [];
3238
+ for (let index = 0; index < lines.length; index += 1) {
3239
+ if (index <= section.start || index >= section.end) continue;
3240
+ if (lines[index] === request.start) startMatches.push(index);
3241
+ if (lines[index] === request.end) endMatches.push(index);
3242
+ }
3243
+ if (startMatches.length !== 1) return {
3244
+ ok: false,
3245
+ code: "part-anchor-not-found",
3246
+ message: "heading \"## " + wanted + "\" start anchor must match exactly one content line"
3247
+ };
3248
+ if (endMatches.length !== 1) return {
3249
+ ok: false,
3250
+ code: "part-anchor-not-found",
3251
+ message: "heading \"## " + wanted + "\" end anchor must match exactly one content line"
3252
+ };
3253
+ const start = startMatches[0];
3254
+ const end = endMatches[0];
3255
+ if (start === void 0 || end === void 0 || start > end) return {
3256
+ ok: false,
3257
+ code: "part-anchor-order",
3258
+ message: "heading \"## " + wanted + "\" start anchor is after end anchor"
3259
+ };
3260
+ if (selected.some((entry) => start < entry.end && end + 1 > entry.start)) return {
3261
+ ok: false,
3262
+ code: "part-anchor-overlaps",
3263
+ message: "heading \"## " + wanted + "\" selection overlaps another selected block"
3264
+ };
3265
+ selected.push({
3266
+ heading: wanted,
3267
+ start,
3268
+ end: end + 1,
3269
+ text: lines.slice(start, end + 1).join(LF$1),
3270
+ requestIndex,
3271
+ anchorStart: request.start,
3272
+ anchorEnd: request.end
3273
+ });
3274
+ }
3275
+ return {
3276
+ ok: true,
3277
+ sections: selected
3278
+ };
3279
+ }
3280
+ //#endregion
3281
+ //#region lib/types/extract-plan.js
3282
+ const LF = String.fromCharCode(10);
3283
+ function sha256$1(text) {
3284
+ return createHash("sha256").update(text).digest("hex");
3285
+ }
3286
+ function unique(values) {
3287
+ return [...new Set(values)];
3288
+ }
3289
+ function computeSupport(parts, residual, sourceName, sourceFiles, targetFiles, externalFiles) {
3290
+ const selectedText = parts.map((part) => part.sourceText).join("\n\n");
3291
+ const selectedRefs = unique(supportRefs(selectedText));
3292
+ const residualRefs = new Set(supportRefs(residual));
3293
+ const unresolved = selectedRefs.filter((ref) => !sourceFiles.includes(ref));
3294
+ if (unresolved.length > 0) return { error: "support-unresolved: " + unresolved.join(", ") };
3295
+ const candidates = selectedRefs.filter((ref) => sourceFiles.includes(ref));
3296
+ const moves = planRehoming(candidates, targetFiles, sourceName).moves;
3297
+ const byFrom = new Map(moves.map((move) => [move.from, move.to]));
3298
+ const exclusive = [];
3299
+ const shared = [];
3300
+ for (const from of candidates) {
3301
+ const to = byFrom.get(from);
3302
+ if (to === void 0) continue;
3303
+ if (residualRefs.has(from)) shared.push({
3304
+ from,
3305
+ to,
3306
+ provenance: sourceName + ":" + from
3307
+ });
3308
+ else exclusive.push({
3309
+ from,
3310
+ to
3311
+ });
3312
+ }
3313
+ const targetFilesWithMoves = unique([...targetFiles, ...moves.map((move) => move.to)]);
3314
+ const external = unique(externalRefs(selectedText));
3315
+ const suppliedRefs = new Set(externalFiles.map((file) => file.ref));
3316
+ const missingExternal = external.filter((ref) => !suppliedRefs.has(ref));
3317
+ const warnings = [];
3318
+ if (missingExternal.length > 0) warnings.push("external-support-not-copied: " + missingExternal.join(", "));
3319
+ const externalClosure = [];
3320
+ for (const file of externalFiles) {
3321
+ if (!external.includes(file.ref)) return { error: "external-ref-not-selected: " + file.ref };
3322
+ if (file.targetPath.startsWith("/") || file.targetPath.includes("..") || !/^(?:references|templates|scripts|assets)\//.test(file.targetPath)) return { error: "external-target-unsafe: " + file.targetPath };
3323
+ if (targetFilesWithMoves.includes(file.targetPath)) return { error: "external-collision: " + file.targetPath };
3324
+ externalClosure.push({
3325
+ from: file.ref,
3326
+ to: file.targetPath,
3327
+ provenance: file.provenance,
3328
+ originalPath: file.originalPath,
3329
+ content: file.content,
3330
+ sha256: file.sha256
3331
+ });
3332
+ }
3333
+ const rewritten = /* @__PURE__ */ new Map();
3334
+ for (const part of parts) {
3335
+ const partRefs = supportRefs(part.sourceText);
3336
+ const rewrite = planReferenceRewrite({
3337
+ content: part.sourceText,
3338
+ files: sourceFiles,
3339
+ moves: moves.filter((move) => partRefs.includes(move.from)),
3340
+ targetFiles: targetFilesWithMoves
3341
+ });
3342
+ rewritten.set(part.heading, applyReferenceRewrite(part.sourceText, rewrite.edits));
3343
+ warnings.push(...rewrite.unresolved.map((ref) => "support-unresolved: " + ref));
3344
+ warnings.push(...rewrite.residualDangling.map((ref) => "support-dangling: " + ref));
3345
+ }
3346
+ for (const part of parts) {
3347
+ let text = rewritten.get(part.heading) ?? part.sourceText;
3348
+ for (const file of externalClosure) text = text.split(file.from).join(file.to);
3349
+ part.movedText = text;
3350
+ }
3351
+ const exclusiveSet = new Set(exclusive.map((entry) => entry.from));
3352
+ const parentExclusive = supportRefs(residual).filter((ref) => exclusiveSet.has(ref));
3353
+ if (parentExclusive.length > 0) return { error: "support-exclusive-referenced-by-parent: " + parentExclusive.join(", ") };
3354
+ const dangling = unique(warnings.filter((item) => item.startsWith("support-dangling: ")).map((item) => item.slice(18)));
3355
+ return {
3356
+ support: {
3357
+ exclusive,
3358
+ shared,
3359
+ external: externalClosure,
3360
+ unresolved: warnings.filter((item) => item.startsWith("support-unresolved: ")),
3361
+ dangling
3362
+ },
3363
+ warnings
3364
+ };
3365
+ }
3366
+ /**
3367
+ * Build a pure extract plan. No IO, no writes: selection, residual, support
3368
+ * closure and the moved text are computed from the supplied bodies/file lists.
3369
+ * @param input - source body, target identity, owner decision and support lists.
3370
+ * @returns a plan, or a refusal with a stable code.
3371
+ */
3372
+ function planExtract(input) {
3373
+ if (input.outcome === "owner-gap") {
3374
+ if (input.owner.mode !== "owner-gap") return {
3375
+ ok: false,
3376
+ code: "owner-gap-mismatch",
3377
+ message: "outcome owner-gap requires an owner-gap decision"
3378
+ };
3379
+ if (input.owner.neededOwner === void 0 || input.owner.neededOwner.trim() === "") return {
3380
+ ok: false,
3381
+ code: "owner-gap-needed-owner",
3382
+ message: "owner-gap requires neededOwner"
3383
+ };
3384
+ } else {
3385
+ if (input.owner.mode !== "covered" || !input.owner.triggerCovers || !input.owner.outcomeCovers) return {
3386
+ ok: false,
3387
+ code: "owner-not-covered",
3388
+ message: "extract/mixed requires a covering owner"
3389
+ };
3390
+ if (!input.targetExists) return {
3391
+ ok: false,
3392
+ code: "target-missing",
3393
+ message: "covered owner target does not exist"
3394
+ };
3395
+ }
3396
+ const selection = selectSections(input.body, input.requests);
3397
+ if (!selection.ok) return {
3398
+ ok: false,
3399
+ code: selection.code,
3400
+ message: selection.message
3401
+ };
3402
+ if (selection.sections.length === 0) return {
3403
+ ok: false,
3404
+ code: "part-empty",
3405
+ message: "at least one part is required"
3406
+ };
3407
+ const parts = selection.sections.map((section) => ({
3408
+ heading: section.heading,
3409
+ anchor: {
3410
+ heading: section.heading,
3411
+ ...section.anchorStart === void 0 ? {} : { start: section.anchorStart },
3412
+ ...section.anchorEnd === void 0 ? {} : { end: section.anchorEnd }
3413
+ },
3414
+ sourceText: section.text,
3415
+ movedText: section.text,
3416
+ bytes: Buffer.byteLength(section.text, "utf8"),
3417
+ sha256: sha256$1(section.text)
3418
+ }));
3419
+ const pointer = "This capability now lives in " + (input.owner.mode === "covered" ? input.owner.name ?? input.targetName : input.owner.neededOwner ?? input.targetName) + "; project-specific facts stay here.";
3420
+ const ordered = [...selection.sections].sort((a, b) => a.start - b.start);
3421
+ const first = ordered[0];
3422
+ if (first === void 0) return {
3423
+ ok: false,
3424
+ code: "part-empty",
3425
+ message: "at least one part is required"
3426
+ };
3427
+ const byStart = new Map(ordered.map((section) => [section.start, section]));
3428
+ const lines = input.body.split(LF);
3429
+ const rebuilt = [];
3430
+ for (let index = 0; index < lines.length; index += 1) {
3431
+ const section = byStart.get(index);
3432
+ if (section !== void 0) {
3433
+ if (index === first.start) rebuilt.push(pointer);
3434
+ index = section.end - 1;
3435
+ } else rebuilt.push(lines[index] ?? "");
3436
+ }
3437
+ const residual = rebuilt.join(LF);
3438
+ if (residual.trim().length === 0) return {
3439
+ ok: false,
3440
+ code: "parent-empty",
3441
+ message: "parent residual is empty"
3442
+ };
3443
+ if (residual.split(LF).filter((line) => line.trim() !== "" && line !== pointer && !line.trimStart().startsWith("#")).length === 0) return {
3444
+ ok: false,
3445
+ code: "parent-hollow-heading",
3446
+ message: "parent residual has no substantive content"
3447
+ };
3448
+ if (input.outcome === "owner-gap") return {
3449
+ ok: true,
3450
+ plan: {
3451
+ source: input.sourceName,
3452
+ target: {
3453
+ name: input.targetName,
3454
+ exists: input.targetExists
3455
+ },
3456
+ outcome: input.outcome,
3457
+ parts,
3458
+ residual,
3459
+ pointer,
3460
+ owner: input.owner,
3461
+ support: {
3462
+ exclusive: [],
3463
+ shared: [],
3464
+ external: [],
3465
+ unresolved: [],
3466
+ dangling: []
3467
+ },
3468
+ warnings: [],
3469
+ ...input.anchors === void 0 ? {} : { anchors: input.anchors }
3470
+ }
3471
+ };
3472
+ const supportResult = computeSupport(parts, residual, input.sourceName, input.sourceFiles, input.targetFiles, input.external ?? []);
3473
+ if ("error" in supportResult) return {
3474
+ ok: false,
3475
+ code: supportResult.error.split(":")[0] ?? "support-invalid",
3476
+ message: supportResult.error
3477
+ };
3478
+ if (supportResult.support.dangling.length > 0) return {
3479
+ ok: false,
3480
+ code: "support-dangling",
3481
+ message: "support-dangling: " + supportResult.support.dangling.join(", ")
3482
+ };
3483
+ return {
3484
+ ok: true,
3485
+ plan: {
3486
+ source: input.sourceName,
3487
+ target: {
3488
+ name: input.targetName,
3489
+ exists: input.targetExists
3490
+ },
3491
+ outcome: input.outcome,
3492
+ parts,
3493
+ residual,
3494
+ pointer,
3495
+ owner: input.owner,
3496
+ support: supportResult.support,
3497
+ warnings: unique(supportResult.warnings),
3498
+ ...input.anchors === void 0 ? {} : { anchors: input.anchors }
3499
+ }
3500
+ };
3501
+ }
3502
+ /**
3503
+ * Project one plan onto the current source/target SKILL.md bytes.
3504
+ *
3505
+ * This is the one place that composes frontmatter + residual + target additions;
3506
+ * preview and the write adapter both call it so they cannot advertise different
3507
+ * bytes. Pure: no IO.
3508
+ * @param plan - plan from planExtract.
3509
+ * @param sourceMd - current source SKILL.md.
3510
+ * @param targetMd - current target SKILL.md.
3511
+ * @returns both next documents, or the refusal that makes projection impossible.
3512
+ */
3513
+ function projectExtractBodies(plan, sourceMd, targetMd) {
3514
+ const sourceBlock = frontmatterBlock(sourceMd);
3515
+ const targetBlock = frontmatterBlock(targetMd);
3516
+ if (!sourceBlock) return {
3517
+ ok: false,
3518
+ message: "source 没有有效 frontmatter"
3519
+ };
3520
+ if (!targetBlock) return {
3521
+ ok: false,
3522
+ message: "target 没有有效 frontmatter"
3523
+ };
3524
+ const sourceHeader = sourceBlock.lines.slice(0, sourceBlock.end + 1).join(LF);
3525
+ const targetNormalized = targetMd.replace(/\r\n/g, LF);
3526
+ const targetLines = targetNormalized.split(LF);
3527
+ for (const part of plan.parts) if (targetLines.some((line) => line.trim() === "## " + part.heading)) return {
3528
+ ok: false,
3529
+ message: "target 已有同名 heading「" + part.heading + "」。"
3530
+ };
3531
+ const additions = plan.parts.map((part) => part.movedText).join("\n\n");
3532
+ const targetRaw = targetNormalized.trimEnd() + "\n\n" + additions + LF;
3533
+ const targetAfter = targetBlock.nl === "\r\n" ? targetRaw.replace(/\n/g, "\r\n") : targetRaw;
3534
+ const sourceRaw = (sourceHeader + plan.residual).replace(/\r\n/g, LF);
3535
+ return {
3536
+ ok: true,
3537
+ projection: {
3538
+ sourceAfter: sourceBlock.nl === "\r\n" ? sourceRaw.replace(/\n/g, "\r\n") : sourceRaw,
3539
+ targetAfter
3540
+ }
3541
+ };
3542
+ }
3543
+ //#endregion
3544
+ //#region lib/types/owner-resolver.js
3545
+ /**
3546
+ * Project the current library listing into owner candidates.
3547
+ *
3548
+ * The source is excluded; a skill with an empty description is named in
3549
+ * excluded rather than silently dropped. Protected candidates remain visible
3550
+ * (so a caller can explain why it refused), but are marked for the writer's
3551
+ * protection gate. Headings reuse h2Sections; no second heading scanner.
3552
+ * @param source - skill being extracted from.
3553
+ * @param summaries - SkillLibrary.list({ withContent: true, withFrontmatter: true }).
3554
+ * @returns stable-name-ordered candidates plus explicit exclusions.
3555
+ */
3556
+ function ownerCandidatesFromSummaries(source, summaries) {
3557
+ const normalized = source.trim();
3558
+ const candidates = [];
3559
+ const excluded = [];
3560
+ for (const summary of summaries) {
3561
+ if (summary.name === normalized) {
3562
+ excluded.push({
3563
+ name: summary.name,
3564
+ reason: "source"
3565
+ });
3566
+ continue;
3567
+ }
3568
+ if (summary.description.trim() === "") {
3569
+ excluded.push({
3570
+ name: summary.name,
3571
+ reason: "unreadable"
3572
+ });
3573
+ continue;
3574
+ }
3575
+ candidates.push({
3576
+ name: summary.name,
3577
+ description: summary.description,
3578
+ ...summary.whenToUse === void 0 ? {} : { whenToUse: summary.whenToUse },
3579
+ headings: h2Sections(summary.content ?? "").map((section) => section.title),
3580
+ protectedBy: summary.protectedBy,
3581
+ protectionUnknown: summary.protectionUnknown,
3582
+ managed: summary.managed
3583
+ });
3584
+ }
3585
+ candidates.sort((left, right) => left.name.localeCompare(right.name));
3586
+ excluded.sort((left, right) => left.name.localeCompare(right.name));
3587
+ return {
3588
+ source: normalized,
3589
+ candidates,
3590
+ excluded
3591
+ };
3592
+ }
3593
+ /**
3594
+ * Find one named candidate in a lookup.
3595
+ * @param lookup - projection from ownerCandidatesFromSummaries.
3596
+ * @param name - exact skill name.
3597
+ * @returns the candidate, or undefined when absent.
3598
+ */
3599
+ function findOwnerCandidate(lookup, name) {
3600
+ return lookup.candidates.find((candidate) => candidate.name === name);
3601
+ }
3602
+ //#endregion
3603
+ //#region lib/types/proposal-view.js
3604
+ /**
3605
+ * Project a pure ExtractPlan into the proposal record every read-only surface consumes.
3606
+ *
3607
+ * This is a projection, not a second evaluator: it keeps the plan's owner decision,
3608
+ * support closure and warnings. It never writes and never invents coverage.
3609
+ * @param plan - plan produced by planExtract.
3610
+ * @param provenance - run id and timestamp for the proposing pass.
3611
+ * @returns a proposal with status proposed.
3612
+ */
3613
+ function proposalFromPlan(plan, provenance) {
3614
+ return {
3615
+ version: 1,
3616
+ id: "extract-" + provenance.runId + "-" + plan.source,
3617
+ source: plan.source,
3618
+ outcome: plan.outcome,
3619
+ parts: plan.parts.map((part) => ({
3620
+ heading: part.heading,
3621
+ anchor: part.anchor,
3622
+ reason: part.sourceText.slice(0, 160)
3623
+ })),
3624
+ owner: plan.owner,
3625
+ support: plan.support,
3626
+ warnings: plan.warnings,
3627
+ provenance: {
3628
+ promptVersion: "v1",
3629
+ runId: provenance.runId,
3630
+ at: provenance.at ?? Date.now()
3631
+ },
3632
+ status: "proposed"
3633
+ };
3634
+ }
3635
+ //#endregion
3636
+ //#region lib/types/state-store.js
3637
+ /**
3638
+ * Evolution home path helpers: the DSH root and `$DSH_HOME/evolution` for
3639
+ * plugin-owned sidecar state (reports, activity store, feedback file,
3640
+ * state-domain data).
3641
+ *
3642
+ * C-10: PATH HELPERS ONLY — despite the file name there is no store
3643
+ * here. Durable evolution state lives in the state stack (evolution-state over
3644
+ * evolution-state-json / -domain); skills and memories live in skill-store.ts
3645
+ * / memory-store.ts. The file name is kept deliberately: renaming it would
3646
+ * touch every family import for zero behavior change, and the audit records
3647
+ * the mismatch as known naming debt.
3648
+ */
3649
+ /**
3650
+ * DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
3651
+ * fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
3652
+ * home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
3653
+ * V8-06 (0.3.47) extends the guard to whitespace.
3654
+ * C-11 correction (S2.2, PLAN 2026-09-16): upstream `resolveDshHome`
3655
+ * (util/home-paths) uses `trim()` ONLY as the ADOPTION test and then uses the
3656
+ * RAW env value — the earlier "same trimmed source" form returned the trimmed
3657
+ * text, so `DSH_HOME=" /x "` produced `/x` where upstream produced the literal
3658
+ * padded path. The value semantics now match upstream line for line:
3659
+ * `const selected = configured ?? (fromEnv !== undefined &&
3660
+ * fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())`.
3661
+ * OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
3662
+ * resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
3663
+ * load-bearing when the skill-catalog shadow made "same tree as the upstream
3664
+ * `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
3665
+ * absolute `<home>/skills` while this value fed a literal `~/x` (a directory
3666
+ * named `~` under the host CWD) or a CWD-relative path — split-brain skill
3667
+ * trees, preset installs the platform never reads, doctor probes of a
3668
+ * directory nothing serves.
3669
+ * S2.2 (PLAN 2026-09-16), final correction: the result is ALWAYS
3670
+ * `resolve(expandHomePath(selected))` — the earlier form returned an
3671
+ * already-absolute value VERBATIM, so a trailing slash or `..` segment
3672
+ * landed unnormalized while upstream normalizes every value. Relative
3673
+ * `DSH_HOME` values change landing spot; a CLEAN absolute home stays
3674
+ * byte-identical (resolve is a no-op on it).
3675
+ */
3676
+ function evolutionRoot(env = process.env) {
3677
+ const fromEnv = env.DSH_HOME;
3678
+ const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), ".dsh");
3679
+ return resolve(selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected);
3680
+ }
3681
+ /** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
3682
+ * state (reports, activity store, feedback file, state-domain data). */
3683
+ function evolutionHome(env = process.env) {
3684
+ return join(evolutionRoot(env), "evolution");
3685
+ }
3686
+ //#endregion
3687
+ //#region lib/types/proposal-report.js
3688
+ /** Directory for the one proposal lane; reports are retention-swept with the other run reports. */
3689
+ function proposalReportDir() {
3690
+ return join(evolutionHome(), "reports");
3691
+ }
3692
+ /** Stable report filename for one proposal; the id may carry path separators. */
3693
+ function proposalReportName(proposal) {
3694
+ return "extract-proposal-" + encodeURIComponent(proposal.id) + ".json";
3695
+ }
3696
+ /**
3697
+ * Persist one proposal as a report. Best-effort: a failed write never blocks the extract.
3698
+ * @param io - evolution IO seam.
3699
+ * @param proposal - proposal to persist.
3700
+ * @param dir - reports directory (tests pass a temp dir).
3701
+ * @returns the written path.
3702
+ */
3703
+ async function writeProposalReport(io, proposal, dir = proposalReportDir()) {
3704
+ const path = join(dir, proposalReportName(proposal));
3705
+ await io.writeText(path, JSON.stringify(proposal));
3706
+ return path;
3707
+ }
3708
+ function isProposal(value) {
3709
+ if (value === null || typeof value !== "object") return false;
3710
+ const candidate = value;
3711
+ return candidate.version === 1 && typeof candidate.id === "string" && typeof candidate.source === "string" && (candidate.outcome === "keep" || candidate.outcome === "extract" || candidate.outcome === "mixed" || candidate.outcome === "owner-gap");
3712
+ }
3713
+ /**
3714
+ * Read the newest proposals. Missing/unreadable reports are skipped, never guessed.
3715
+ * @param io - evolution IO seam.
3716
+ * @param dir - reports directory.
3717
+ * @param limit - newest rows to return.
3718
+ * @returns valid proposals, newest filename first.
3719
+ */
3720
+ async function readProposalReports(io, dir = proposalReportDir(), limit = 10) {
3721
+ const listed = await probeList(io, dir);
3722
+ if (isUnknown(listed) || !isPresent(listed)) return [];
3723
+ const names = listed.value.filter((name) => name.startsWith("extract-proposal-") && name.endsWith(".json"));
3724
+ names.sort((a, b) => b.localeCompare(a));
3725
+ const proposals = [];
3726
+ for (const name of names.slice(0, limit)) {
3727
+ const raw = await probeText(io, join(dir, name));
3728
+ if (!isPresent(raw)) continue;
3729
+ let parsed;
3730
+ try {
3731
+ parsed = JSON.parse(raw.value);
3732
+ } catch {
3733
+ continue;
3734
+ }
3735
+ if (isProposal(parsed)) proposals.push(parsed);
3736
+ }
3737
+ return proposals;
3738
+ }
3739
+ //#endregion
3740
+ //#region lib/types/external-support.js
3741
+ const MAX_FILE_BYTES = 1024 * 1024;
3742
+ const MAX_TOTAL_BYTES = 4 * 1024 * 1024;
3743
+ function targetPathFor(ref) {
3744
+ let value = ref.replace(/^\/+/, "");
3745
+ while (value.startsWith("../")) value = value.slice(3);
3746
+ const support = value.match(/(?:^|\/)(references|templates|scripts|assets)\/(.+)$/);
3747
+ if (support !== null) return support[1] + "/" + support[2];
3748
+ return "references/" + basename(value);
3749
+ }
3750
+ function annotate(content, originalPath) {
3751
+ const note = "external source: " + originalPath;
3752
+ const extension = originalPath.slice(originalPath.lastIndexOf(".")).toLowerCase();
3753
+ if (extension === ".md") return "<!-- " + note + " -->\n\n" + content;
3754
+ const prefix = extension === ".py" || extension === ".sh" || extension === ".rb" ? "# " : "// ";
3755
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs" || extension === ".ts" || extension === ".tsx" || extension === ".py" || extension === ".sh" || extension === ".rb") {
3756
+ if (content.startsWith("#!")) {
3757
+ const end = content.indexOf(String.fromCharCode(10));
3758
+ if (end >= 0) return content.slice(0, end + 1) + prefix + note + String.fromCharCode(10) + content.slice(end + 1);
3759
+ }
3760
+ return prefix + note + String.fromCharCode(10) + content;
3761
+ }
3762
+ return content;
3763
+ }
3764
+ function inside(root, candidate) {
3765
+ const rel = relative(root, candidate);
3766
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
3767
+ }
3768
+ /**
3769
+ * Read and annotate external support files under one explicit root. The caller owns
3770
+ * the root choice; this module only enforces containment, target-path safety, size
3771
+ * limits and determinism.
3772
+ * @param body - selected source markdown body.
3773
+ * @param sourceDir - source skill directory.
3774
+ * @param allowedRoot - canonical root external files must stay under.
3775
+ * @param targetFiles - existing target support paths.
3776
+ * @returns resolved external entries, or a refusal.
3777
+ */
3778
+ async function readExternalSupport(body, sourceDir, allowedRoot, targetFiles) {
3779
+ const refs = externalRefs(body);
3780
+ if (refs.length === 0) return {
3781
+ ok: true,
3782
+ files: []
3783
+ };
3784
+ let root;
3785
+ try {
3786
+ root = await realpath(allowedRoot);
3787
+ } catch {
3788
+ return {
3789
+ ok: false,
3790
+ message: "external support 根目录不可读:" + allowedRoot
3791
+ };
3792
+ }
3793
+ const files = [];
3794
+ const targets = new Set(targetFiles);
3795
+ let total = 0;
3796
+ for (const ref of refs) {
3797
+ const candidate = isAbsolute(ref) ? resolve(ref) : resolve(sourceDir, ref);
3798
+ let canonical;
3799
+ try {
3800
+ canonical = await realpath(candidate);
3801
+ } catch {
3802
+ return {
3803
+ ok: false,
3804
+ message: "external support 文件不存在:" + ref
3805
+ };
3806
+ }
3807
+ if (!inside(root, canonical)) return {
3808
+ ok: false,
3809
+ message: "external support 越界:" + ref
3810
+ };
3811
+ const info = await stat(canonical);
3812
+ if (!info.isFile()) return {
3813
+ ok: false,
3814
+ message: "external support 不是普通文件:" + ref
3815
+ };
3816
+ if (info.size > MAX_FILE_BYTES) return {
3817
+ ok: false,
3818
+ message: "external support 超过单文件上限:" + ref
3819
+ };
3820
+ total += info.size;
3821
+ if (total > MAX_TOTAL_BYTES) return {
3822
+ ok: false,
3823
+ message: "external support 超过总上限:" + ref
3824
+ };
3825
+ const targetPath = targetPathFor(ref);
3826
+ if (targets.has(targetPath)) return {
3827
+ ok: false,
3828
+ message: "external support 目标冲突:" + targetPath
3829
+ };
3830
+ targets.add(targetPath);
3831
+ const annotated = annotate(await readFile(canonical, "utf8"), canonical);
3832
+ files.push({
3833
+ ref,
3834
+ originalPath: canonical,
3835
+ targetPath,
3836
+ content: annotated,
3837
+ sha256: createHash("sha256").update(annotated).digest("hex"),
3838
+ provenance: "external:" + canonical
3839
+ });
3840
+ }
3841
+ return {
3842
+ ok: true,
3843
+ files
3844
+ };
3845
+ }
3846
+ //#endregion
3847
+ //#region lib/types/extract-prompt.js
3848
+ /**
3849
+ * The frozen semantic prompt for skill extraction. One constant, consumed by
3850
+ * explicit extract agents and tests; it is deliberately not part of the review
3851
+ * prompt bundle, and no review path injects it.
3852
+ */
3853
+ const EXTRACT_PROMPT_VERSION = "v1";
3854
+ const EXTRACT_PROMPT_V1 = `Inspect the source skill, its linked support files, and the candidate owners
3855
+ in the library. A part of the skill (one or more sections) is a separate
3856
+ capability when it has its own trigger and outcome, can be used without the
3857
+ parent context, and leaves the parent coherent. If it is separate and a
3858
+ class-level owner covers it, extract that part. If no owner covers it, report
3859
+ the owner gap and name the class-level owner that would be needed. If it mixes
3860
+ a reusable capability with parent-specific facts, return mixed and name only
3861
+ the reusable part. Otherwise keep it. Keep, extract, mixed, and owner-gap are
3862
+ all valid outcomes; no split is required.`;
3863
+ //#endregion
3050
3864
  //#region lib/types/cost.js
3051
3865
  /**
3052
3866
  * Context-cost accounting for skill bodies (design §5.2).
@@ -4492,57 +5306,6 @@ function buildLearnPrompt(userRequest) {
4492
5306
  ].join("\n");
4493
5307
  }
4494
5308
  //#endregion
4495
- //#region lib/types/state-store.js
4496
- /**
4497
- * Evolution home path helpers: the DSH root and `$DSH_HOME/evolution` for
4498
- * plugin-owned sidecar state (reports, activity store, feedback file,
4499
- * state-domain data).
4500
- *
4501
- * C-10: PATH HELPERS ONLY — despite the file name there is no store
4502
- * here. Durable evolution state lives in the state stack (evolution-state over
4503
- * evolution-state-json / -domain); skills and memories live in skill-store.ts
4504
- * / memory-store.ts. The file name is kept deliberately: renaming it would
4505
- * touch every family import for zero behavior change, and the audit records
4506
- * the mismatch as known naming debt.
4507
- */
4508
- /**
4509
- * DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
4510
- * fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
4511
- * home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
4512
- * V8-06 (0.3.47) extends the guard to whitespace.
4513
- * C-11 correction (S2.2, PLAN 2026-09-16): upstream `resolveDshHome`
4514
- * (util/home-paths) uses `trim()` ONLY as the ADOPTION test and then uses the
4515
- * RAW env value — the earlier "same trimmed source" form returned the trimmed
4516
- * text, so `DSH_HOME=" /x "` produced `/x` where upstream produced the literal
4517
- * padded path. The value semantics now match upstream line for line:
4518
- * `const selected = configured ?? (fromEnv !== undefined &&
4519
- * fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())`.
4520
- * OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
4521
- * resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
4522
- * load-bearing when the skill-catalog shadow made "same tree as the upstream
4523
- * `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
4524
- * absolute `<home>/skills` while this value fed a literal `~/x` (a directory
4525
- * named `~` under the host CWD) or a CWD-relative path — split-brain skill
4526
- * trees, preset installs the platform never reads, doctor probes of a
4527
- * directory nothing serves.
4528
- * S2.2 (PLAN 2026-09-16), final correction: the result is ALWAYS
4529
- * `resolve(expandHomePath(selected))` — the earlier form returned an
4530
- * already-absolute value VERBATIM, so a trailing slash or `..` segment
4531
- * landed unnormalized while upstream normalizes every value. Relative
4532
- * `DSH_HOME` values change landing spot; a CLEAN absolute home stays
4533
- * byte-identical (resolve is a no-op on it).
4534
- */
4535
- function evolutionRoot(env = process.env) {
4536
- const fromEnv = env.DSH_HOME;
4537
- const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), ".dsh");
4538
- return resolve(selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected);
4539
- }
4540
- /** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
4541
- * state (reports, activity store, feedback file, state-domain data). */
4542
- function evolutionHome(env = process.env) {
4543
- return join(evolutionRoot(env), "evolution");
4544
- }
4545
- //#endregion
4546
5309
  //#region lib/types/serial.js
4547
5310
  /**
4548
5311
  * A process-local serial task queue: each task starts only after the previous
@@ -7053,6 +7816,7 @@ const CONTENT_ACTIONS = [
7053
7816
  "patch",
7054
7817
  "consolidate",
7055
7818
  "restructure",
7819
+ "extract",
7056
7820
  "restore",
7057
7821
  "merge"
7058
7822
  ];
@@ -7460,6 +8224,7 @@ function versionActionKind(action) {
7460
8224
  case "archive": return "archive";
7461
8225
  case "consolidate": return "consolidate";
7462
8226
  case "restructure": return "restructure";
8227
+ case "extract": return "extract";
7463
8228
  case "merge": return "merge";
7464
8229
  case "write_file": return "support-write";
7465
8230
  case "remove_file": return "support-remove";
@@ -9524,7 +10289,8 @@ const READ_REQUIRED_ACTIONS = Object.freeze([
9524
10289
  "delete",
9525
10290
  "write_file",
9526
10291
  "remove_file",
9527
- "restructure"
10292
+ "restructure",
10293
+ "extract"
9528
10294
  ]);
9529
10295
  /**
9530
10296
  * Does this one op write a skill the caller never read?
@@ -9735,95 +10501,6 @@ function skillRowCells(skill) {
9735
10501
  };
9736
10502
  }
9737
10503
  //#endregion
9738
- //#region lib/types/document-body.js
9739
- /**
9740
- * What the panel shows when it renders one version as a document.
9741
- *
9742
- * Two decisions, both made here so the preview and the rendered diff cannot drift apart:
9743
- *
9744
- * - FRONTMATTER IS NOT DOCUMENT TEXT. A SKILL.md opens with a YAML block; the renderer read its
9745
- * closing `---` as a setext underline, so the metadata line became the document's biggest heading
9746
- * and the real title sat below it (measured on the installed panel 2026-09-28). The block is
9747
- * stripped for DISPLAY only: the raw view and the body route still carry every byte, and the
9748
- * panel already shows the name and description on the row and in the left column.
9749
- * - STRIPPING FAILS OPEN. Only a block that the family's own reader accepts is removed. A document
9750
- * whose text merely starts with a thematic break, a block with no closing line, or a frontmatter
9751
- * whose body is empty is returned UNCHANGED — showing a document's first line is always better
9752
- * than showing nothing.
9753
- *
9754
- * `readingStats` exists for the other half of the reader's honesty: a preview that stops early has
9755
- * to be able to say HOW much it is not showing.
9756
- * @module @lmzhen/dsh-evolution-core/document-body
9757
- */
9758
- /**
9759
- * The document a reader sees: the body without its leading frontmatter block.
9760
- * @param content - the stored bytes of one version.
9761
- * @returns the text to render; unchanged when there is no frontmatter the reader accepts.
9762
- */
9763
- function displayBodyOf(content) {
9764
- const block = frontmatterBlock(content);
9765
- if (block === null) return content;
9766
- if (parseFrontmatter(content) === null) return content;
9767
- return block.lines.slice(block.end + 1).join("\n").replace(/^\s*\n/, "");
9768
- }
9769
- /**
9770
- * How much of a version the panel is showing.
9771
- * @param full - the whole stored text.
9772
- * @param shown - the part the route handed over.
9773
- * @returns the counts, so the panel can say what it is not showing.
9774
- */
9775
- function readingStats(full, shown) {
9776
- return {
9777
- totalChars: full.length,
9778
- shownChars: shown.length,
9779
- truncated: shown.length < full.length
9780
- };
9781
- }
9782
- /**
9783
- * The H2 sections of a body, in document order — the ONE scan (G3).
9784
- *
9785
- * Both the restructure candidate list and the store anchor validation call this function, so a
9786
- * candidate can only name a heading that exists EXACTLY ONCE and has content: the two rejections a
9787
- * hand-written anchor used to hit ("no such heading", "ambiguous anchor") stop being reachable.
9788
- * H2 is the section boundary (a section spans to the next H2 or EOF), matching the store.
9789
- * @param body - the markdown after the frontmatter block.
9790
- * @returns one row per H2 heading, with `count` filled in a second pass.
9791
- */
9792
- function h2Sections(body) {
9793
- const lines = body.split(String.fromCharCode(10));
9794
- const rows = [];
9795
- for (let i = 0; i < lines.length; i += 1) {
9796
- const match = /^#{2}\s+(.+?)\s*$/.exec(lines[i] ?? "");
9797
- if (match === null) continue;
9798
- const title = (match[1] ?? "").trim();
9799
- let end = lines.length;
9800
- for (let j = i + 1; j < lines.length; j += 1) if (/^#{2}\s/.test(lines[j] ?? "")) {
9801
- end = j;
9802
- break;
9803
- }
9804
- rows.push({
9805
- title,
9806
- start: i,
9807
- end,
9808
- count: 0
9809
- });
9810
- }
9811
- for (const row of rows) row.count = rows.filter((other) => other.title === row.title).length;
9812
- return rows;
9813
- }
9814
- /**
9815
- * The sections a restructure proposal may name: heading unique, section non-empty. Derived from
9816
- * `h2Sections`, i.e. from the SAME scan the store validates against.
9817
- * @param body - the markdown after the frontmatter block.
9818
- * @returns move-ready pairs in document order; empty when nothing qualifies.
9819
- */
9820
- function restructureCandidates(body) {
9821
- return h2Sections(body).filter((section) => section.count === 1 && section.end > section.start + 1).map((section) => ({
9822
- heading: section.title,
9823
- toFile: "references/" + section.title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-+|-+$/g, "") + ".md"
9824
- }));
9825
- }
9826
- //#endregion
9827
10504
  //#region lib/types/drift-signals.js
9828
10505
  /**
9829
10506
  * Library-level drift signals for the maintenance subagent (design 011).
@@ -10626,48 +11303,20 @@ function validateSupportPath(filePath) {
10626
11303
  }
10627
11304
  return null;
10628
11305
  }
10629
- /**
10630
- * Support-directory references in a markdown body (009 kernel): `references/…`,
10631
- * `templates/…`, `scripts/…`, `assets/…` relative links — any extension and
10632
- * nested paths (v7 audit P3-1: `.md`-only matching missed `scripts/run.sh` and
10633
- * `references/sub/x.md`, both of which dangle just like a `.md` link once the
10634
- * source package is archived). Pure — who checks them and what the verdict is
10635
- * belongs to the caller's context (a moved/appended body whose references
10636
- * travel with an archived source is a dangling link; a restructure pointer is
10637
- * a fresh link to a file written in the same plan). `..` traversal is a
10638
- * different defect class (path validation owns it) and is not a support link.
10639
- */
10640
- function supportRefs(content) {
10641
- const refs = [];
10642
- for (const match of content.matchAll(/\b(?:references|templates|scripts|assets)\/[A-Za-z0-9._/-]+/g)) {
10643
- const ref = match[0];
10644
- if (ref.includes("..")) continue;
10645
- refs.push(ref);
10646
- }
10647
- return refs;
10648
- }
10649
11306
  function planRestructureSections(body, moves) {
11307
+ const selection = selectSections(body, moves.map((move) => ({ heading: move.heading })));
11308
+ if (!selection.ok) return { error: selection.message };
10650
11309
  const lines = body.split("\n");
10651
- const sections = h2Sections(body);
10652
- const spans = [];
10653
- for (const move of moves) {
10654
- const wanted = move.heading.trim();
10655
- const matching = sections.filter((section) => section.title === wanted);
10656
- if (matching.length === 0) return { error: `no "## ${wanted}" heading in the body` };
10657
- if (matching.length > 1) return { error: `heading "## ${wanted}" appears ${matching.length} times (ambiguous anchor)` };
10658
- const section = matching[0];
10659
- if (section === void 0) return { error: `no "## ${wanted}" heading in the body` };
10660
- const { start, end } = section;
10661
- if (end === start + 1) return { error: `heading "## ${wanted}" has an empty section` };
10662
- if (spans.some((span) => start === span.start)) return { error: `heading "## ${wanted}" is moved twice` };
10663
- spans.push({
10664
- start,
10665
- end,
10666
- rel: move.toFile,
10667
- heading: wanted,
10668
- text: lines.slice(start, end).join("\n")
10669
- });
10670
- }
11310
+ const spans = selection.sections.map((section) => {
11311
+ const move = moves[section.requestIndex];
11312
+ return {
11313
+ start: section.start,
11314
+ end: section.end,
11315
+ rel: move?.toFile ?? "",
11316
+ heading: section.heading,
11317
+ text: section.text
11318
+ };
11319
+ });
10671
11320
  const byStart = new Map(spans.map((span) => [span.start, span]));
10672
11321
  const rebuilt = [];
10673
11322
  for (let i = 0; i < lines.length; i += 1) {
@@ -12762,48 +13411,245 @@ var SkillLibrary = class {
12762
13411
  * restructure (and future reference-mode consolidations) never implement
12763
13412
  * two-phase commit themselves.
12764
13413
  */
12765
- async applyTreeChange(plan) {
12766
- const name = plan.name.trim();
12767
- const badName = this.badName(name, { allowReserved: true });
12768
- if (badName) return {
13414
+ /**
13415
+ * Apply one G1 ExtractPlan as one cross-skill unit. This is a thin adapter:
13416
+ * selection, residual, pointer and support closure were already decided by
13417
+ * planExtract; this method only projects them into ordered kernel operations.
13418
+ * @param plan - pure plan from extract-plan.ts.
13419
+ * @param origin - write face recorded in the audit ledger.
13420
+ * @returns the kernel result; a refusal writes nothing.
13421
+ */
13422
+ async extract(plan, origin = this.authority.origin) {
13423
+ if (plan.outcome === "owner-gap" || plan.owner.mode !== "covered") return {
12769
13424
  ok: false,
12770
- message: badName
13425
+ message: "提取被拒绝:owner-gap 不是写操作。"
12771
13426
  };
12772
- const dir = this.dirOf(name);
12773
- if (!await this.io.readText(join(dir, "SKILL.md"))) return {
13427
+ if (plan.support.unresolved.length > 0 || plan.support.dangling.length > 0) return {
12774
13428
  ok: false,
12775
- message: `技能「${name}」不存在。`
13429
+ message: "提取被拒绝:support closure 存在 unresolved/dangling。"
12776
13430
  };
12777
- const protection = plan.protection === "write" ? await this.writeProtection(name, plan.origin) : plan.protection === "delete" ? await this.deleteProtection(name) : null;
12778
- if (protection) return {
13431
+ if (plan.parts.length === 0) return {
12779
13432
  ok: false,
12780
- message: `技能「${name}」受保护(${protection})。`
13433
+ message: "提取被拒绝:没有 part。"
13434
+ };
13435
+ const source = plan.source.trim();
13436
+ const target = plan.target.name.trim();
13437
+ if (source === target) return {
13438
+ ok: false,
13439
+ message: "提取被拒绝:source 与 target 相同。"
13440
+ };
13441
+ if (plan.target.exists !== true) return {
13442
+ ok: false,
13443
+ message: "提取被拒绝:target 不存在;owner-gap 不自动 create。"
13444
+ };
13445
+ const sourceDir = this.dirOf(source);
13446
+ const targetDir = this.dirOf(target);
13447
+ const sourceMd = await this.io.readText(join(sourceDir, "SKILL.md"));
13448
+ const targetMd = await this.io.readText(join(targetDir, "SKILL.md"));
13449
+ if (sourceMd === null) return {
13450
+ ok: false,
13451
+ message: "技能「" + source + "」不存在。"
13452
+ };
13453
+ if (targetMd === null) return {
13454
+ ok: false,
13455
+ message: "技能「" + target + "」不存在。"
13456
+ };
13457
+ if (plan.anchors !== void 0) {
13458
+ const sourceHash = contentHash(sourceMd);
13459
+ const targetHash = contentHash(targetMd);
13460
+ if (sourceHash !== plan.anchors.sourceMdSha256 || targetHash !== plan.anchors.targetMdSha256) return {
13461
+ ok: false,
13462
+ message: "提取被拒绝:source/target 在计划之后发生了变化;请重新计划。"
13463
+ };
13464
+ }
13465
+ const projected = projectExtractBodies(plan, sourceMd, targetMd);
13466
+ if (!projected.ok) return {
13467
+ ok: false,
13468
+ message: "提取被拒绝:" + projected.message
13469
+ };
13470
+ const sourceNew = projected.projection.sourceAfter;
13471
+ const targetNew = projected.projection.targetAfter;
13472
+ const ops = [];
13473
+ for (const move of [...plan.support.shared, ...plan.support.exclusive]) {
13474
+ const fromPath = join(sourceDir, ...move.from.split("/"));
13475
+ const toPath = join(targetDir, ...move.to.split("/"));
13476
+ const content = await this.io.readText(fromPath);
13477
+ if (content === null) return {
13478
+ ok: false,
13479
+ message: "提取被拒绝:support 文件不存在 " + move.from
13480
+ };
13481
+ const previous = await this.readForCas(toPath);
13482
+ ops.push({
13483
+ kind: "write",
13484
+ target: toPath,
13485
+ content,
13486
+ expected: previous,
13487
+ skill: target
13488
+ });
13489
+ }
13490
+ for (const file of plan.support.external) {
13491
+ if (file.content === void 0) return {
13492
+ ok: false,
13493
+ message: "提取被拒绝:external support 缺少已验证的内容 " + file.from
13494
+ };
13495
+ if (file.to.startsWith("/") || file.to.includes("..") || !/^(?:references|templates|scripts|assets)\//.test(file.to)) return {
13496
+ ok: false,
13497
+ message: "提取被拒绝:external target 不安全 " + file.to
13498
+ };
13499
+ const toPath = join(targetDir, ...file.to.split("/"));
13500
+ const previous = await this.readForCas(toPath);
13501
+ ops.push({
13502
+ kind: "write",
13503
+ target: toPath,
13504
+ content: file.content,
13505
+ expected: previous,
13506
+ skill: target
13507
+ });
13508
+ }
13509
+ ops.push({
13510
+ kind: "write",
13511
+ target: join(targetDir, "SKILL.md"),
13512
+ content: targetNew,
13513
+ expected: targetMd,
13514
+ skill: target
13515
+ });
13516
+ for (const move of plan.support.exclusive) {
13517
+ const sourcePath = join(sourceDir, ...move.from.split("/"));
13518
+ const previous = await this.readForCas(sourcePath);
13519
+ if (previous === null) return {
13520
+ ok: false,
13521
+ message: "提取被拒绝:待删除 support 不存在 " + move.from
13522
+ };
13523
+ ops.push({
13524
+ kind: "delete",
13525
+ target: sourcePath,
13526
+ expected: previous,
13527
+ skill: source
13528
+ });
13529
+ }
13530
+ ops.push({
13531
+ kind: "write",
13532
+ target: join(sourceDir, "SKILL.md"),
13533
+ content: sourceNew,
13534
+ expected: sourceMd,
13535
+ skill: source
13536
+ });
13537
+ return this.applyTreeChange({
13538
+ name: source,
13539
+ origin,
13540
+ protection: "write",
13541
+ roots: [{
13542
+ name: source,
13543
+ protection: "write"
13544
+ }, {
13545
+ name: target,
13546
+ protection: "write"
13547
+ }],
13548
+ writes: [],
13549
+ ops,
13550
+ auditAction: "extract",
13551
+ auditSummary: "extract " + plan.parts.map((part) => part.heading).join(", ") + " from " + source + " to " + target,
13552
+ eventAction: "extract"
13553
+ });
13554
+ }
13555
+ async applyTreeChange(plan) {
13556
+ const primaryName = plan.name.trim();
13557
+ const declared = plan.roots ?? [{
13558
+ name: primaryName,
13559
+ protection: plan.protection
13560
+ }];
13561
+ const roots = [];
13562
+ for (const root of declared) {
13563
+ const name = root.name.trim();
13564
+ if (roots.some((entry) => entry.name === name)) return {
13565
+ ok: false,
13566
+ message: "目录树变更声明了重复的根「" + name + "」。"
13567
+ };
13568
+ const badName = this.badName(name, { allowReserved: true });
13569
+ if (badName) return {
13570
+ ok: false,
13571
+ message: badName
13572
+ };
13573
+ const dir = this.dirOf(name);
13574
+ if (!await this.io.readText(join(dir, "SKILL.md"))) return {
13575
+ ok: false,
13576
+ message: "技能「" + name + "」不存在。"
13577
+ };
13578
+ const protection = root.protection === "write" ? await this.writeProtection(name, plan.origin) : root.protection === "delete" ? await this.deleteProtection(name) : null;
13579
+ if (protection) return {
13580
+ ok: false,
13581
+ message: "技能「" + name + "」受保护(" + protection + ")。"
13582
+ };
13583
+ roots.push({
13584
+ name,
13585
+ protection: root.protection,
13586
+ dir
13587
+ });
13588
+ }
13589
+ if (!roots.some((root) => root.name === primaryName)) return {
13590
+ ok: false,
13591
+ message: "目录树变更的 primary skill「" + primaryName + "」不在 roots 中。"
13592
+ };
13593
+ const provided = plan.ops ?? [...plan.writes.map((write) => ({
13594
+ kind: "write",
13595
+ ...write
13596
+ })), ...(plan.deletes ?? []).map((del) => ({
13597
+ kind: "delete",
13598
+ ...del
13599
+ }))];
13600
+ if (provided.length === 0) return {
13601
+ ok: false,
13602
+ message: "目录树变更为空。"
12781
13603
  };
12782
13604
  const landing = [];
12783
- for (const write of plan.writes) {
13605
+ for (const op of provided) {
13606
+ const owner = roots.find((root) => {
13607
+ const rel = relative(root.dir, op.target);
13608
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
13609
+ });
13610
+ if (owner === void 0) return {
13611
+ ok: false,
13612
+ message: "目标不属于任何声明的技能根:" + op.target
13613
+ };
13614
+ const skill = op.skill ?? owner.name;
13615
+ if (!roots.some((root) => root.name === skill)) return {
13616
+ ok: false,
13617
+ message: "目标声明的技能「" + skill + "」不在 roots 中:" + op.target
13618
+ };
12784
13619
  let previous;
12785
13620
  try {
12786
- previous = await this.io.readText(write.target);
13621
+ previous = await this.io.readText(op.target);
12787
13622
  } catch (error) {
12788
13623
  return {
12789
13624
  ok: false,
12790
- message: `拒绝目录树变更:无法安全预读 ${write.target}(${error instanceof Error ? error.message : String(error)});未执行任何写入`
13625
+ message: "拒绝目录树变更:无法安全预读 " + op.target + "(" + (error instanceof Error ? error.message : String(error)) + ");未执行任何写入"
12791
13626
  };
12792
13627
  }
12793
- if (Buffer.byteLength(write.content, "utf8") > this.limits.maxSkillFileBytes) return {
12794
- ok: false,
12795
- message: `写入超过 ${this.limits.maxSkillFileBytes} 字节:${write.target}`
12796
- };
12797
- const threat = this.contentThreatBlock(write.content);
12798
- if (threat) return {
13628
+ if (op.kind === "delete" && previous === null) return {
12799
13629
  ok: false,
12800
- message: threat
13630
+ message: "拒绝目录树变更:删除目标不存在 " + op.target
12801
13631
  };
13632
+ if (op.kind === "write") {
13633
+ if (Buffer.byteLength(op.content, "utf8") > this.limits.maxSkillFileBytes) return {
13634
+ ok: false,
13635
+ message: "写入超过 " + this.limits.maxSkillFileBytes + " 字节:" + op.target
13636
+ };
13637
+ const threat = this.contentThreatBlock(op.content);
13638
+ if (threat) return {
13639
+ ok: false,
13640
+ message: threat
13641
+ };
13642
+ }
13643
+ const rel = relative(owner.dir, op.target).split(String.fromCharCode(92)).join("/");
12802
13644
  landing.push({
12803
- target: write.target,
12804
- content: write.content,
13645
+ kind: op.kind,
13646
+ target: op.target,
13647
+ content: op.kind === "write" ? op.content : null,
12805
13648
  previous,
12806
- expected: write.expected
13649
+ expected: op.expected,
13650
+ skill,
13651
+ dir: owner.dir,
13652
+ rel
12807
13653
  });
12808
13654
  }
12809
13655
  const written = [];
@@ -12822,11 +13668,12 @@ var SkillLibrary = class {
12822
13668
  drift.seen = true;
12823
13669
  return current;
12824
13670
  }
12825
- return entry.content;
13671
+ return entry.kind === "delete" ? null : entry.content;
12826
13672
  });
12827
- if (!ran.done) throw new Error(`internal error: the write transaction for ${entry.target} did not invoke the task; nothing was written`);
12828
- if (drift.seen) throw new Error(`concurrent modification detected: ${entry.target} changed after the plan was computed (a concurrent writer won the race); no further writes were performed`);
12829
- } else await this.io.writeText(entry.target, entry.content);
13673
+ if (!ran.done) throw new Error("internal error: the write transaction for " + entry.target + " did not invoke the task; nothing was written");
13674
+ if (drift.seen) throw new Error("concurrent modification detected: " + entry.target + " changed after the plan was computed (a concurrent writer won the race); no further writes were performed");
13675
+ } else if (entry.kind === "delete") await this.io.remove(entry.target);
13676
+ else await this.io.writeText(entry.target, entry.content ?? "");
12830
13677
  } catch (error) {
12831
13678
  if (!isCommittedOnly(error)) throw error;
12832
13679
  durabilityWarning = error instanceof Error ? error.message : String(error);
@@ -12847,42 +13694,43 @@ var SkillLibrary = class {
12847
13694
  const reason = error instanceof Error ? error.message : String(error);
12848
13695
  if (stuck.length === 0) return {
12849
13696
  ok: false,
12850
- message: `目录树变更失败,已回滚:${reason}`
13697
+ message: "目录树变更失败,已回滚:" + reason
12851
13698
  };
12852
13699
  return {
12853
13700
  ok: false,
12854
- message: `目录树变更失败:${reason}。回滚无法恢复 ${stuck.join(", ")}:可恢复性由当前快照决定:${await this.snapshotRecoveryAvailability()} —— 若上面不是「最新快照」,请重新应用这次变更。`
13701
+ message: "目录树变更失败:" + reason + "。回滚无法恢复 " + stuck.join(", ") + ":可恢复性由当前快照决定:" + await this.snapshotRecoveryAvailability() + " —— 若上面不是「最新快照」,请重新应用这次变更。"
12855
13702
  };
12856
13703
  }
12857
13704
  const unitId = plan.unitId ?? "u-" + Date.now().toString(36) + "-" + (nextUnitSeq += 1).toString(36);
12858
- for (const land of landing) {
12859
- const rel = land.target.split(/[\\/]/).pop() === "SKILL.md" ? void 0 : land.target.slice(dir.length + 1).split("\\").join("/");
12860
- const unitHead = land === landing[0] ? plan.unitHead ?? {
12861
- moves: landing.filter((l) => l.target.split(/[\\/]/).pop() !== "SKILL.md").map((l) => {
12862
- const p = l.target.slice(dir.length + 1).split(String.fromCharCode(92)).join(String.fromCharCode(47));
12863
- return {
12864
- path: p,
12865
- kind: l.previous === null ? "created" : "moved",
12866
- to: p,
12867
- ...l.previous === null ? {} : { beforeRef: contentHash(l.previous) },
12868
- afterRef: contentHash(l.content)
12869
- };
12870
- }).filter((m, idx, all) => all.map((x) => x.path).lastIndexOf(m.path) === idx),
12871
- sources: [{ skill: name }]
12872
- } : void 0;
12873
- const auditedBefore = land.previous;
12874
- const auditedAfter = land.content;
12875
- await this.audit(name, plan.auditAction, auditedBefore, auditedAfter, plan.auditSummary, rel, unitId, unitHead, plan.origin);
13705
+ const defaultHead = {
13706
+ moves: landing.filter((entry) => basename(entry.target) !== "SKILL.md").map((entry) => ({
13707
+ path: entry.rel,
13708
+ kind: entry.kind === "delete" ? "removed" : entry.previous === null ? "created" : "moved",
13709
+ ...entry.previous === null ? {} : { beforeRef: contentHash(entry.previous) },
13710
+ ...entry.kind === "delete" || entry.content === null ? {} : { afterRef: contentHash(entry.content) }
13711
+ })),
13712
+ sources: roots.map((root) => ({ skill: root.name }))
13713
+ };
13714
+ const affected = /* @__PURE__ */ new Set();
13715
+ for (let index = 0; index < landing.length; index += 1) {
13716
+ const entry = landing[index];
13717
+ if (entry === void 0) continue;
13718
+ await this.audit(entry.skill, plan.auditAction, entry.previous, entry.kind === "delete" ? null : entry.content, plan.auditSummary, entry.rel === "SKILL.md" ? void 0 : entry.rel, unitId, index === 0 ? plan.unitHead ?? defaultHead : void 0, plan.origin);
13719
+ affected.add(entry.skill);
12876
13720
  }
12877
- this.notifyMutation({
12878
- action: plan.eventAction,
12879
- name,
12880
- skillDir: dir
12881
- });
13721
+ for (const skill of affected) {
13722
+ const dir = roots.find((root) => root.name === skill)?.dir;
13723
+ if (dir !== void 0) this.notifyMutation({
13724
+ action: plan.eventAction,
13725
+ name: skill,
13726
+ skillDir: dir
13727
+ });
13728
+ }
13729
+ const primaryDir = roots.find((root) => root.name === primaryName)?.dir ?? roots[0]?.dir ?? "";
12882
13730
  return {
12883
13731
  ok: true,
12884
- message: durabilityWarning === "" ? `${plan.eventAction} "${name}" succeeded.` : `${plan.eventAction} "${name}" succeeded (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:${durabilityWarning})`,
12885
- path: dir
13732
+ message: durabilityWarning === "" ? plan.eventAction + " \"" + primaryName + "\" succeeded." : plan.eventAction + " \"" + primaryName + "\" succeeded (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:" + durabilityWarning + ")",
13733
+ path: primaryDir
12886
13734
  };
12887
13735
  }
12888
13736
  /**
@@ -15933,4 +16781,4 @@ function isIgnorableEvent(event) {
15933
16781
  return classifySessionEvent(event) !== "model";
15934
16782
  }
15935
16783
  //#endregion
15936
- export { 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, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ADMISSION, 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_PROBE_TIMEOUT_MS, 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_RUN_RECORDS, 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, DEFAULT_WRITE_CONFIRM_MODE, DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS, DIAGNOSTICS_FACT_KEYS, DIAGNOSTICS_RUNTIME_KEYS, DIAGNOSTICS_VIEW_SCHEMA_VERSION, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_PRESET_IDS, 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, HOST_ROUTES, INSTANCE_KEYS, KEEP_LINE_RE, LONG_RUNS, LOW_QUALITY_THRESHOLD, MAINTAINER_PRESET_ID, 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, MODEL_SESSION_EVENT_TYPES as MODEL_SESSION_EVENTS, MODEL_SESSION_EVENT_TYPES, 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, PLATFORM_LINE, PLATFORM_SERVICE_PROBES, 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, RESTRICTED_PRESET_DISPLAY, RESTRICTED_TOOLS_ROW_ID, RESTRICTOR_MODULE, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, REVIEWER_PRESET_ID, RULES, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, STORE_LEDGER, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPORT_SPRAWL_FILES, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNIT_JOURNAL_PHASES, UNIT_JOURNAL_SCHEMA_VERSION, UNIT_STORAGE_SCHEMA_VERSION, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, UnitTransactions, VERSION_SUMMARY_MAX_CHARS, absentHostServices, acquireTreeWriteLock, admissionVerdict, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assertUnitJournalTransition, assessStructureHealth, atomicWriteFiles, authoringFeedback, authorityFrom, backupsReachability, blobPath, bodyCost, buildCuratorRunReport, buildHealthBaseline, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, candidateSkills, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, classifySessionEvent, clearReviewChannel, collectReadSkillNames, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, countReferenceFiles, createGateSet, createSubagentChannel, daysSinceIso, declaredReportStamp, describeReferenceRewrite, diffFindings, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, ensureSkillFrontmatter, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, executeUnitUndo, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, h2Sections, historyIndexFile, historyReachability, historyRoot, hostSurface, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isIgnorableEvent, isLandmark, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isRecord, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, json, landmarkVerdict, latestActivityAt, latestVersionAt, lifecycleCandidate, lintBody, lintSkill, listEventArchives, liveBool, liveEnum, liveNumber, liveString, loadMutations, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, methodNotAllowed, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, partitionVersions, payloadText, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, planUnitUndoAcross, policyStageLimits, probeAbsent, probeChannelState, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, protectedSkillNamesOf, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonObject, readNumberParam, readParam, readTreeLock, readUnitEvidence, readUnitIndex, readUnitJournal, readUnitReceipt, readUnitSnapshot, readingStats, recordMutation, recordVersions, redactSecrets, referencedBlobs, registerHostRoutes, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsReachability, reportsSweepLockTarget, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, resolveUnitMembers, resolveUnitMembersFromIndexes, restructureCandidates, retainEventArchives, retirementReport, reviewPrompt, revisionAnchor, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runWriteGates, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopeOfNearestManifest, scopedPackageName, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillLibraryUnitBackend, skillReadNameOf, skillRowCells, skillStateKey, skillWriteGateContext, skillsRoot, staleRouteStubs, subagentChannelState, supportFileReadOf, supportRefs, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, treeLockFile, trimPatternBoundaries, unhookedSupportPointers, unitEvidenceDigest, unitEvidencePath, unitIndexPath, unitJournalPath, unitJournalPhaseRank, unitMembersAreComplete, unitReceiptPath, unitSnapshotPath, unitSnapshotsDir, unitStorageKey, unwrapLive, updateSuppressedNames, upsertUnitIndex, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, validateSupportPath, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, withTreeWrite, withTreeWriteLock, writeDurableTmp, writePathSymbols, writeUnitEvidence, writeUnitJournal, writeUnitReceipt, writeUnitSnapshot, yamlPlainScalarNeedsQuotes };
16784
+ export { 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, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ADMISSION, 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_PROBE_TIMEOUT_MS, 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_RUN_RECORDS, 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, DEFAULT_WRITE_CONFIRM_MODE, DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS, DIAGNOSTICS_FACT_KEYS, DIAGNOSTICS_RUNTIME_KEYS, DIAGNOSTICS_VIEW_SCHEMA_VERSION, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_PRESET_IDS, EVOLUTION_WRITE_TOOLS, EXTRACT_PROMPT_V1, EXTRACT_PROMPT_VERSION, 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, HOST_ROUTES, INSTANCE_KEYS, KEEP_LINE_RE, LONG_RUNS, LOW_QUALITY_THRESHOLD, MAINTAINER_PRESET_ID, 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, MODEL_SESSION_EVENT_TYPES as MODEL_SESSION_EVENTS, MODEL_SESSION_EVENT_TYPES, 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, PLATFORM_LINE, PLATFORM_SERVICE_PROBES, 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, RESTRICTED_PRESET_DISPLAY, RESTRICTED_TOOLS_ROW_ID, RESTRICTOR_MODULE, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, REVIEWER_PRESET_ID, RULES, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, STORE_LEDGER, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPORT_SPRAWL_FILES, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNIT_JOURNAL_PHASES, UNIT_JOURNAL_SCHEMA_VERSION, UNIT_STORAGE_SCHEMA_VERSION, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, UnitTransactions, VERSION_SUMMARY_MAX_CHARS, absentHostServices, acquireTreeWriteLock, admissionVerdict, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assertUnitJournalTransition, assessStructureHealth, atomicWriteFiles, authoringFeedback, authorityFrom, backupsReachability, blobPath, bodyCost, buildCuratorRunReport, buildHealthBaseline, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, candidateSkills, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, classifySessionEvent, clearReviewChannel, collectReadSkillNames, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, countReferenceFiles, createGateSet, createSubagentChannel, daysSinceIso, declaredReportStamp, describeReferenceRewrite, diffFindings, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, ensureSkillFrontmatter, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, executeUnitUndo, externalRefs, filterUnreadSkillOps, findDriftSignal, findOwnerCandidate, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, h2Sections, historyIndexFile, historyReachability, historyRoot, hostSurface, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isIgnorableEvent, isLandmark, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isRecord, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, json, landmarkVerdict, latestActivityAt, latestVersionAt, lifecycleCandidate, lintBody, lintSkill, listEventArchives, liveBool, liveEnum, liveNumber, liveString, loadMutations, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, methodNotAllowed, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, ownerCandidatesFromSummaries, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, partitionVersions, payloadText, persistedWriteSite, persistedWriteSites, pickWithPolicy, planExtract, planNamespaceMigration, planReferenceRewrite, planRehoming, planUnitUndoAcross, policyStageLimits, probeAbsent, probeChannelState, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectExtractBodies, projectionText, proposalFromPlan, proposalReportDir, proposalReportName, protectedSkillNamesOf, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readExternalSupport, readHistoryIndex, readJsonObject, readNumberParam, readParam, readProposalReports, readTreeLock, readUnitEvidence, readUnitIndex, readUnitJournal, readUnitReceipt, readUnitSnapshot, readingStats, recordMutation, recordVersions, redactSecrets, referencedBlobs, registerHostRoutes, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsReachability, reportsSweepLockTarget, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, resolveUnitMembers, resolveUnitMembersFromIndexes, restructureCandidates, retainEventArchives, retirementReport, reviewPrompt, revisionAnchor, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runWriteGates, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopeOfNearestManifest, scopedPackageName, scopedProbeReport, selectSections, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillLibraryUnitBackend, skillReadNameOf, skillRowCells, skillStateKey, skillWriteGateContext, skillsRoot, staleRouteStubs, subagentChannelState, supportFileReadOf, supportRefs, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, treeLockFile, trimPatternBoundaries, unhookedSupportPointers, unitEvidenceDigest, unitEvidencePath, unitIndexPath, unitJournalPath, unitJournalPhaseRank, unitMembersAreComplete, unitReceiptPath, unitSnapshotPath, unitSnapshotsDir, unitStorageKey, unwrapLive, updateSuppressedNames, upsertUnitIndex, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, validateSupportPath, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, withTreeWrite, withTreeWriteLock, writeDurableTmp, writePathSymbols, writeProposalReport, writeUnitEvidence, writeUnitJournal, writeUnitReceipt, writeUnitSnapshot, yamlPlainScalarNeedsQuotes };