@lmzhen/dsh-evolution-core 0.30.30 → 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 +1162 -273
- package/lib/types/external-support.d.ts +19 -0
- package/lib/types/external-support.js +96 -0
- package/lib/types/extract-plan.d.ts +127 -0
- package/lib/types/extract-plan.js +232 -0
- package/lib/types/extract-prompt.d.ts +8 -0
- package/lib/types/extract-prompt.js +16 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/index.js +8 -0
- package/lib/types/owner-resolver.d.ts +40 -0
- package/lib/types/owner-resolver.js +49 -0
- package/lib/types/prompts.d.ts +7 -7
- package/lib/types/prompts.js +51 -10
- package/lib/types/proposal-report.d.ts +23 -0
- package/lib/types/proposal-report.js +61 -0
- package/lib/types/proposal-view.d.ts +42 -0
- package/lib/types/proposal-view.js +24 -0
- package/lib/types/section-selection.d.ts +38 -0
- package/lib/types/section-selection.js +78 -0
- package/lib/types/skill-history.d.ts +1 -1
- package/lib/types/skill-history.js +2 -1
- package/lib/types/skill-reads.js +1 -1
- package/lib/types/skill-store.d.ts +10 -12
- package/lib/types/skill-store.js +197 -173
- package/lib/types/support-refs.d.ts +17 -0
- package/lib/types/support-refs.js +35 -0
- package/package.json +1 -1
|
@@ -300,7 +300,7 @@ export declare function loadVersionContent(root: string, io: EvolutionIoLike, na
|
|
|
300
300
|
*/
|
|
301
301
|
export declare function recordVersions(root: string, io: EvolutionIoLike, input: VersionRecordInput, keep: number): Promise<RecordedVersions | null>;
|
|
302
302
|
/** What kind of change one recorded version carries, as a CLOSED set. */
|
|
303
|
-
export type VersionActionKind = 'baseline' | 'create' | 'patch' | 'update' | 'restore' | 'delete' | 'archive' | 'consolidate' | 'restructure' | 'merge' | 'support-write' | 'support-remove' | 'other';
|
|
303
|
+
export type VersionActionKind = 'baseline' | 'create' | 'patch' | 'update' | 'restore' | 'delete' | 'archive' | 'consolidate' | 'restructure' | 'extract' | 'merge' | 'support-write' | 'support-remove' | 'other';
|
|
304
304
|
/**
|
|
305
305
|
* Classify one version action for display. Fail-closed like {@link versionTarget}: an action this
|
|
306
306
|
* module does not know reads as `other` rather than being guessed, and `tests/skill-history.spec.ts`
|
|
@@ -85,7 +85,7 @@ export function orderVersions(versions) {
|
|
|
85
85
|
/** The actions that record a SUPPORT FILE's bytes (they share the skill's version index). */
|
|
86
86
|
const SUPPORT_ACTIONS = ['write_file', 'remove_file'];
|
|
87
87
|
/** The actions that record the skill's own SKILL.md body. */
|
|
88
|
-
const CONTENT_ACTIONS = ['create', 'update', 'patch', 'consolidate', 'restructure', 'restore', 'merge'];
|
|
88
|
+
const CONTENT_ACTIONS = ['create', 'update', 'patch', 'consolidate', 'restructure', 'extract', 'restore', 'merge'];
|
|
89
89
|
/**
|
|
90
90
|
* Which artifact one recorded version holds, from the action that produced it.
|
|
91
91
|
* @param action - the entry's action label.
|
|
@@ -610,6 +610,7 @@ export function versionActionKind(action) {
|
|
|
610
610
|
case 'archive': return 'archive';
|
|
611
611
|
case 'consolidate': return 'consolidate';
|
|
612
612
|
case 'restructure': return 'restructure';
|
|
613
|
+
case 'extract': return 'extract';
|
|
613
614
|
case 'merge': return 'merge';
|
|
614
615
|
case 'write_file': return 'support-write';
|
|
615
616
|
case 'remove_file': return 'support-remove';
|
package/lib/types/skill-reads.js
CHANGED
|
@@ -26,7 +26,7 @@ export function sessionReadSkillNames(ctx, session) {
|
|
|
26
26
|
}
|
|
27
27
|
/** Actions whose target must have been read first (create is authorship, so it is exempt). */
|
|
28
28
|
export const READ_REQUIRED_ACTIONS = Object.freeze([
|
|
29
|
-
'edit', 'update', 'patch', 'delete', 'write_file', 'remove_file', 'restructure',
|
|
29
|
+
'edit', 'update', 'patch', 'delete', 'write_file', 'remove_file', 'restructure', 'extract',
|
|
30
30
|
]);
|
|
31
31
|
/**
|
|
32
32
|
* Does this one op write a skill the caller never read?
|
|
@@ -36,6 +36,7 @@ import { type WriteAuthority } from './write-authority.ts';
|
|
|
36
36
|
import { transactIo, type EvolutionIoLike } from './io.ts';
|
|
37
37
|
import { type Probe } from './probe.ts';
|
|
38
38
|
import type { SkillLimits } from './limits.ts';
|
|
39
|
+
import { type ExtractPlanDraft } from './extract-plan.ts';
|
|
39
40
|
import { type MutationRecord } from './mutations.ts';
|
|
40
41
|
import { type RevisionAnchor, type RevisionFile } from './skill-revision.ts';
|
|
41
42
|
import { type UnitHead, type SkillVersion } from './skill-history.ts';
|
|
@@ -323,18 +324,6 @@ export declare function markerEntryName(marker: 'bundled' | 'hub-installed' | 'p
|
|
|
323
324
|
* Single source shared with the plan validator. */
|
|
324
325
|
export declare function validateRestructureTarget(filePath: string): string | null;
|
|
325
326
|
export declare function validateSupportPath(filePath: string): string | null;
|
|
326
|
-
/**
|
|
327
|
-
* Support-directory references in a markdown body (009 kernel): `references/…`,
|
|
328
|
-
* `templates/…`, `scripts/…`, `assets/…` relative links — any extension and
|
|
329
|
-
* nested paths (v7 audit P3-1: `.md`-only matching missed `scripts/run.sh` and
|
|
330
|
-
* `references/sub/x.md`, both of which dangle just like a `.md` link once the
|
|
331
|
-
* source package is archived). Pure — who checks them and what the verdict is
|
|
332
|
-
* belongs to the caller's context (a moved/appended body whose references
|
|
333
|
-
* travel with an archived source is a dangling link; a restructure pointer is
|
|
334
|
-
* a fresh link to a file written in the same plan). `..` traversal is a
|
|
335
|
-
* different defect class (path validation owns it) and is not a support link.
|
|
336
|
-
*/
|
|
337
|
-
export declare function supportRefs(content: string): string[];
|
|
338
327
|
export declare class SkillLibrary {
|
|
339
328
|
readonly root: string;
|
|
340
329
|
readonly limits: SkillLimits;
|
|
@@ -681,6 +670,15 @@ export declare class SkillLibrary {
|
|
|
681
670
|
* restructure (and future reference-mode consolidations) never implement
|
|
682
671
|
* two-phase commit themselves.
|
|
683
672
|
*/
|
|
673
|
+
/**
|
|
674
|
+
* Apply one G1 ExtractPlan as one cross-skill unit. This is a thin adapter:
|
|
675
|
+
* selection, residual, pointer and support closure were already decided by
|
|
676
|
+
* planExtract; this method only projects them into ordered kernel operations.
|
|
677
|
+
* @param plan - pure plan from extract-plan.ts.
|
|
678
|
+
* @param origin - write face recorded in the audit ledger.
|
|
679
|
+
* @returns the kernel result; a refusal writes nothing.
|
|
680
|
+
*/
|
|
681
|
+
extract(plan: ExtractPlanDraft, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
684
682
|
private applyTreeChange;
|
|
685
683
|
/**
|
|
686
684
|
* Restore one skill from `.archive/` back to the active root. Hermes-style
|
package/lib/types/skill-store.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* re-probes after its copies so a mid-copy writer demotes to `skipped`
|
|
33
33
|
* (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
|
|
34
34
|
*/
|
|
35
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
35
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
36
36
|
import { authorityFrom } from "./write-authority.js";
|
|
37
37
|
import { resolveCitations } from "./citations.js";
|
|
38
38
|
import { scanContentThreats } from "./threats.js";
|
|
@@ -45,7 +45,9 @@ import { applyReferenceRewrite, describeReferenceRewrite, planReferenceRewrite,
|
|
|
45
45
|
import { exceedsContentLimit, frontmatterBlock, normalizeFrontmatter, parseFrontmatter, shrinksOverLimit, validateFrontmatter } from "./frontmatter.js";
|
|
46
46
|
import { FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, trimPatternBoundaries, fuzzyPatch } from "./fuzzy-match.js";
|
|
47
47
|
import { makeSerialQueue } from "./serial.js";
|
|
48
|
-
import {
|
|
48
|
+
import { supportRefs } from "./support-refs.js";
|
|
49
|
+
import { selectSections } from "./section-selection.js";
|
|
50
|
+
import { projectExtractBodies } from "./extract-plan.js";
|
|
49
51
|
import { diffFindings } from "./skill-lint.js";
|
|
50
52
|
import { contentHash, loadMutations, recordMutation } from "./mutations.js";
|
|
51
53
|
import { revisionFingerprint, revisionSupportDigest } from "./skill-revision.js";
|
|
@@ -383,58 +385,24 @@ export function validateSupportPath(filePath) {
|
|
|
383
385
|
}
|
|
384
386
|
return null;
|
|
385
387
|
}
|
|
386
|
-
/**
|
|
387
|
-
* Support-directory references in a markdown body (009 kernel): `references/…`,
|
|
388
|
-
* `templates/…`, `scripts/…`, `assets/…` relative links — any extension and
|
|
389
|
-
* nested paths (v7 audit P3-1: `.md`-only matching missed `scripts/run.sh` and
|
|
390
|
-
* `references/sub/x.md`, both of which dangle just like a `.md` link once the
|
|
391
|
-
* source package is archived). Pure — who checks them and what the verdict is
|
|
392
|
-
* belongs to the caller's context (a moved/appended body whose references
|
|
393
|
-
* travel with an archived source is a dangling link; a restructure pointer is
|
|
394
|
-
* a fresh link to a file written in the same plan). `..` traversal is a
|
|
395
|
-
* different defect class (path validation owns it) and is not a support link.
|
|
396
|
-
*/
|
|
397
|
-
export function supportRefs(content) {
|
|
398
|
-
const refs = [];
|
|
399
|
-
for (const match of content.matchAll(/\b(?:references|templates|scripts|assets)\/[A-Za-z0-9._/-]+/g)) {
|
|
400
|
-
const ref = match[0];
|
|
401
|
-
if (ref.includes('..'))
|
|
402
|
-
continue;
|
|
403
|
-
refs.push(ref);
|
|
404
|
-
}
|
|
405
|
-
return refs;
|
|
406
|
-
}
|
|
407
388
|
function planRestructureSections(body, moves) {
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
389
|
+
// One scan (h2Sections) and one selection rule; extract reuses the same
|
|
390
|
+
// selector for start/end sub-blocks, so restructure and extract cannot drift
|
|
391
|
+
// on heading ambiguity, empty sections or duplicate moves.
|
|
392
|
+
const selection = selectSections(body, moves.map(move => ({ heading: move.heading })));
|
|
393
|
+
if (!selection.ok)
|
|
394
|
+
return { error: selection.message };
|
|
412
395
|
const lines = body.split('\n');
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (section === undefined)
|
|
424
|
-
return { error: `no "## ${wanted}" heading in the body` };
|
|
425
|
-
const { start, end } = section;
|
|
426
|
-
if (end === start + 1)
|
|
427
|
-
return { error: `heading "## ${wanted}" has an empty section` };
|
|
428
|
-
if (spans.some(span => start === span.start))
|
|
429
|
-
return { error: `heading "## ${wanted}" is moved twice` };
|
|
430
|
-
spans.push({
|
|
431
|
-
start,
|
|
432
|
-
end,
|
|
433
|
-
rel: move.toFile,
|
|
434
|
-
heading: wanted,
|
|
435
|
-
text: lines.slice(start, end).join('\n'),
|
|
436
|
-
});
|
|
437
|
-
}
|
|
396
|
+
const spans = selection.sections.map(section => {
|
|
397
|
+
const move = moves[section.requestIndex];
|
|
398
|
+
return {
|
|
399
|
+
start: section.start,
|
|
400
|
+
end: section.end,
|
|
401
|
+
rel: move?.toFile ?? '',
|
|
402
|
+
heading: section.heading,
|
|
403
|
+
text: section.text,
|
|
404
|
+
};
|
|
405
|
+
});
|
|
438
406
|
// Rebuild the body line-wise: a moved section collapses to its pointer line.
|
|
439
407
|
const byStart = new Map(spans.map(span => [span.start, span]));
|
|
440
408
|
const rebuilt = [];
|
|
@@ -2849,45 +2817,151 @@ export class SkillLibrary {
|
|
|
2849
2817
|
* restructure (and future reference-mode consolidations) never implement
|
|
2850
2818
|
* two-phase commit themselves.
|
|
2851
2819
|
*/
|
|
2820
|
+
/**
|
|
2821
|
+
* Apply one G1 ExtractPlan as one cross-skill unit. This is a thin adapter:
|
|
2822
|
+
* selection, residual, pointer and support closure were already decided by
|
|
2823
|
+
* planExtract; this method only projects them into ordered kernel operations.
|
|
2824
|
+
* @param plan - pure plan from extract-plan.ts.
|
|
2825
|
+
* @param origin - write face recorded in the audit ledger.
|
|
2826
|
+
* @returns the kernel result; a refusal writes nothing.
|
|
2827
|
+
*/
|
|
2828
|
+
async extract(plan, origin = this.authority.origin) {
|
|
2829
|
+
if (plan.outcome === 'owner-gap' || plan.owner.mode !== 'covered')
|
|
2830
|
+
return { ok: false, message: '提取被拒绝:owner-gap 不是写操作。' };
|
|
2831
|
+
if (plan.support.unresolved.length > 0 || plan.support.dangling.length > 0)
|
|
2832
|
+
return { ok: false, message: '提取被拒绝:support closure 存在 unresolved/dangling。' };
|
|
2833
|
+
if (plan.parts.length === 0)
|
|
2834
|
+
return { ok: false, message: '提取被拒绝:没有 part。' };
|
|
2835
|
+
const source = plan.source.trim();
|
|
2836
|
+
const target = plan.target.name.trim();
|
|
2837
|
+
if (source === target)
|
|
2838
|
+
return { ok: false, message: '提取被拒绝:source 与 target 相同。' };
|
|
2839
|
+
if (plan.target.exists !== true)
|
|
2840
|
+
return { ok: false, message: '提取被拒绝:target 不存在;owner-gap 不自动 create。' };
|
|
2841
|
+
const sourceDir = this.dirOf(source);
|
|
2842
|
+
const targetDir = this.dirOf(target);
|
|
2843
|
+
const sourceMd = await this.io.readText(join(sourceDir, 'SKILL.md'));
|
|
2844
|
+
const targetMd = await this.io.readText(join(targetDir, 'SKILL.md'));
|
|
2845
|
+
if (sourceMd === null)
|
|
2846
|
+
return { ok: false, message: '技能「' + source + '」不存在。' };
|
|
2847
|
+
if (targetMd === null)
|
|
2848
|
+
return { ok: false, message: '技能「' + target + '」不存在。' };
|
|
2849
|
+
if (plan.anchors !== undefined) {
|
|
2850
|
+
const sourceHash = contentHash(sourceMd);
|
|
2851
|
+
const targetHash = contentHash(targetMd);
|
|
2852
|
+
if (sourceHash !== plan.anchors.sourceMdSha256 || targetHash !== plan.anchors.targetMdSha256) {
|
|
2853
|
+
return { ok: false, message: '提取被拒绝:source/target 在计划之后发生了变化;请重新计划。' };
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
const projected = projectExtractBodies(plan, sourceMd, targetMd);
|
|
2857
|
+
if (!projected.ok)
|
|
2858
|
+
return { ok: false, message: '提取被拒绝:' + projected.message };
|
|
2859
|
+
const sourceNew = projected.projection.sourceAfter;
|
|
2860
|
+
const targetNew = projected.projection.targetAfter;
|
|
2861
|
+
const ops = [];
|
|
2862
|
+
for (const move of [...plan.support.shared, ...plan.support.exclusive]) {
|
|
2863
|
+
const fromPath = join(sourceDir, ...move.from.split('/'));
|
|
2864
|
+
const toPath = join(targetDir, ...move.to.split('/'));
|
|
2865
|
+
const content = await this.io.readText(fromPath);
|
|
2866
|
+
if (content === null)
|
|
2867
|
+
return { ok: false, message: '提取被拒绝:support 文件不存在 ' + move.from };
|
|
2868
|
+
const previous = await this.readForCas(toPath);
|
|
2869
|
+
ops.push({ kind: 'write', target: toPath, content, expected: previous, skill: target });
|
|
2870
|
+
}
|
|
2871
|
+
for (const file of plan.support.external) {
|
|
2872
|
+
if (file.content === undefined)
|
|
2873
|
+
return { ok: false, message: '提取被拒绝:external support 缺少已验证的内容 ' + file.from };
|
|
2874
|
+
if (file.to.startsWith('/') || file.to.includes('..') || !/^(?:references|templates|scripts|assets)\//.test(file.to)) {
|
|
2875
|
+
return { ok: false, message: '提取被拒绝:external target 不安全 ' + file.to };
|
|
2876
|
+
}
|
|
2877
|
+
const toPath = join(targetDir, ...file.to.split('/'));
|
|
2878
|
+
const previous = await this.readForCas(toPath);
|
|
2879
|
+
ops.push({ kind: 'write', target: toPath, content: file.content, expected: previous, skill: target });
|
|
2880
|
+
}
|
|
2881
|
+
ops.push({ kind: 'write', target: join(targetDir, 'SKILL.md'), content: targetNew, expected: targetMd, skill: target });
|
|
2882
|
+
for (const move of plan.support.exclusive) {
|
|
2883
|
+
const sourcePath = join(sourceDir, ...move.from.split('/'));
|
|
2884
|
+
const previous = await this.readForCas(sourcePath);
|
|
2885
|
+
if (previous === null)
|
|
2886
|
+
return { ok: false, message: '提取被拒绝:待删除 support 不存在 ' + move.from };
|
|
2887
|
+
ops.push({ kind: 'delete', target: sourcePath, expected: previous, skill: source });
|
|
2888
|
+
}
|
|
2889
|
+
ops.push({ kind: 'write', target: join(sourceDir, 'SKILL.md'), content: sourceNew, expected: sourceMd, skill: source });
|
|
2890
|
+
return this.applyTreeChange({
|
|
2891
|
+
name: source,
|
|
2892
|
+
origin,
|
|
2893
|
+
protection: 'write',
|
|
2894
|
+
roots: [{ name: source, protection: 'write' }, { name: target, protection: 'write' }],
|
|
2895
|
+
writes: [],
|
|
2896
|
+
ops,
|
|
2897
|
+
auditAction: 'extract',
|
|
2898
|
+
auditSummary: 'extract ' + plan.parts.map(part => part.heading).join(', ') + ' from ' + source + ' to ' + target,
|
|
2899
|
+
eventAction: 'extract',
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2852
2902
|
async applyTreeChange(plan) {
|
|
2853
|
-
const
|
|
2854
|
-
const
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2903
|
+
const primaryName = plan.name.trim();
|
|
2904
|
+
const declared = plan.roots ?? [{ name: primaryName, protection: plan.protection }];
|
|
2905
|
+
const roots = [];
|
|
2906
|
+
for (const root of declared) {
|
|
2907
|
+
const name = root.name.trim();
|
|
2908
|
+
if (roots.some(entry => entry.name === name))
|
|
2909
|
+
return { ok: false, message: '目录树变更声明了重复的根「' + name + '」。' };
|
|
2910
|
+
const badName = this.badName(name, { allowReserved: true });
|
|
2911
|
+
if (badName)
|
|
2912
|
+
return { ok: false, message: badName };
|
|
2913
|
+
const dir = this.dirOf(name);
|
|
2914
|
+
const body = await this.io.readText(join(dir, 'SKILL.md'));
|
|
2915
|
+
if (!body)
|
|
2916
|
+
return { ok: false, message: '技能「' + name + '」不存在。' };
|
|
2917
|
+
const protection = root.protection === 'write'
|
|
2918
|
+
? await this.writeProtection(name, plan.origin)
|
|
2919
|
+
: root.protection === 'delete'
|
|
2920
|
+
? await this.deleteProtection(name)
|
|
2921
|
+
: null;
|
|
2922
|
+
if (protection)
|
|
2923
|
+
return { ok: false, message: '技能「' + name + '」受保护(' + protection + ')。' };
|
|
2924
|
+
roots.push({ name, protection: root.protection, dir });
|
|
2925
|
+
}
|
|
2926
|
+
if (!roots.some(root => root.name === primaryName)) {
|
|
2927
|
+
return { ok: false, message: '目录树变更的 primary skill「' + primaryName + '」不在 roots 中。' };
|
|
2928
|
+
}
|
|
2929
|
+
const provided = plan.ops ?? [
|
|
2930
|
+
...plan.writes.map(write => ({ kind: 'write', ...write })),
|
|
2931
|
+
...(plan.deletes ?? []).map(del => ({ kind: 'delete', ...del })),
|
|
2932
|
+
];
|
|
2933
|
+
if (provided.length === 0)
|
|
2934
|
+
return { ok: false, message: '目录树变更为空。' };
|
|
2872
2935
|
const landing = [];
|
|
2873
|
-
for (const
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2936
|
+
for (const op of provided) {
|
|
2937
|
+
const owner = roots.find(root => {
|
|
2938
|
+
const rel = relative(root.dir, op.target);
|
|
2939
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
2940
|
+
});
|
|
2941
|
+
if (owner === undefined)
|
|
2942
|
+
return { ok: false, message: '目标不属于任何声明的技能根:' + op.target };
|
|
2943
|
+
const skill = op.skill ?? owner.name;
|
|
2944
|
+
if (!roots.some(root => root.name === skill))
|
|
2945
|
+
return { ok: false, message: '目标声明的技能「' + skill + '」不在 roots 中:' + op.target };
|
|
2877
2946
|
let previous;
|
|
2878
2947
|
try {
|
|
2879
|
-
previous = await this.io.readText(
|
|
2948
|
+
previous = await this.io.readText(op.target);
|
|
2880
2949
|
}
|
|
2881
2950
|
catch (error) {
|
|
2882
|
-
return { ok: false, message:
|
|
2951
|
+
return { ok: false, message: '拒绝目录树变更:无法安全预读 ' + op.target + '(' + (error instanceof Error ? error.message : String(error)) + ');未执行任何写入' };
|
|
2883
2952
|
}
|
|
2884
|
-
if (
|
|
2885
|
-
return { ok: false, message:
|
|
2953
|
+
if (op.kind === 'delete' && previous === null)
|
|
2954
|
+
return { ok: false, message: '拒绝目录树变更:删除目标不存在 ' + op.target };
|
|
2955
|
+
if (op.kind === 'write') {
|
|
2956
|
+
if (Buffer.byteLength(op.content, 'utf8') > this.limits.maxSkillFileBytes) {
|
|
2957
|
+
return { ok: false, message: '写入超过 ' + this.limits.maxSkillFileBytes + ' 字节:' + op.target };
|
|
2958
|
+
}
|
|
2959
|
+
const threat = this.contentThreatBlock(op.content);
|
|
2960
|
+
if (threat)
|
|
2961
|
+
return { ok: false, message: threat };
|
|
2886
2962
|
}
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2889
|
-
return { ok: false, message: threat };
|
|
2890
|
-
landing.push({ target: write.target, content: write.content, previous, expected: write.expected });
|
|
2963
|
+
const rel = relative(owner.dir, op.target).split(String.fromCharCode(92)).join('/');
|
|
2964
|
+
landing.push({ kind: op.kind, target: op.target, content: op.kind === 'write' ? op.content : null, previous, expected: op.expected, skill, dir: owner.dir, rel });
|
|
2891
2965
|
}
|
|
2892
2966
|
const written = [];
|
|
2893
2967
|
let durabilityWarning = '';
|
|
@@ -2895,61 +2969,31 @@ export class SkillLibrary {
|
|
|
2895
2969
|
for (const entry of landing) {
|
|
2896
2970
|
try {
|
|
2897
2971
|
if (this.transact) {
|
|
2898
|
-
// v22 (LOCK-1): CAS write — the previous blind writeText committed
|
|
2899
|
-
// plan-time bytes over whatever was on disk, so a concurrent
|
|
2900
|
-
// patch/update (another process, shared DSH_HOME) between the
|
|
2901
|
-
// pre-read above and this write was silently overwritten (lost
|
|
2902
|
-
// update; both writers reported success). The task only commits
|
|
2903
|
-
// when the disk still holds the pre-read baseline; any drift
|
|
2904
|
-
// aborts the plan into the rollback below.
|
|
2905
|
-
// v24 (V24-01): the CAS baseline is the caller's PLAN-TIME bytes
|
|
2906
|
-
// (`expected`) when the write carries them — the pre-read above
|
|
2907
|
-
// only supplies the rollback bytes. Lock-1's own pre-read baseline
|
|
2908
|
-
// still left a window for appends/merges built from an earlier
|
|
2909
|
-
// read: a concurrent write landing between planning and the
|
|
2910
|
-
// pre-read became the CAS baseline, so plan-time content
|
|
2911
|
-
// (composed WITHOUT it) committed cleanly over it — lost update,
|
|
2912
|
-
// both writers successful. Comparing against the plan-time bytes
|
|
2913
|
-
// closes the whole planning→commit span.
|
|
2914
|
-
// P3 (v22; gate fix 0.3.66): the flag lives in a holder so the
|
|
2915
|
-
// control-flow analysis (which cannot see the transact callback's
|
|
2916
|
-
// assignment) does not narrow it to a literal `false`.
|
|
2917
2972
|
const baseline = entry.expected === undefined ? entry.previous : entry.expected;
|
|
2918
2973
|
const drift = { seen: false };
|
|
2919
|
-
// S1.4 (v37 P2-7): a transact backend that never invokes the task
|
|
2920
|
-
// (the V6-19 contract) would leave BOTH holders at their initial
|
|
2921
|
-
// values and this write would be reported as a success.
|
|
2922
2974
|
const ran = { done: false };
|
|
2923
2975
|
await this.transact(this.io, entry.target, (current) => {
|
|
2924
|
-
// v43 audit (FLOW3-1): per-attempt reset. This callback re-runs on a
|
|
2925
|
-
// lock-loss retry, and a stale `drift.seen` from the previous
|
|
2926
|
-
// attempt threw "concurrent modification" even when the retry's own
|
|
2927
|
-
// write had landed — a partial tree change reported as a failure.
|
|
2928
2976
|
drift.seen = false;
|
|
2929
2977
|
ran.done = true;
|
|
2930
2978
|
if (current !== baseline) {
|
|
2931
2979
|
drift.seen = true;
|
|
2932
2980
|
return current;
|
|
2933
2981
|
}
|
|
2934
|
-
return entry.content;
|
|
2982
|
+
return entry.kind === 'delete' ? null : entry.content;
|
|
2935
2983
|
});
|
|
2936
|
-
if (!ran.done)
|
|
2937
|
-
throw new Error(
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2984
|
+
if (!ran.done)
|
|
2985
|
+
throw new Error('internal error: the write transaction for ' + entry.target + ' did not invoke the task; nothing was written');
|
|
2986
|
+
if (drift.seen)
|
|
2987
|
+
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');
|
|
2988
|
+
}
|
|
2989
|
+
else if (entry.kind === 'delete') {
|
|
2990
|
+
await this.io.remove(entry.target);
|
|
2942
2991
|
}
|
|
2943
2992
|
else {
|
|
2944
|
-
|
|
2945
|
-
// constructed without cross-process exclusion anyway).
|
|
2946
|
-
await this.io.writeText(entry.target, entry.content);
|
|
2993
|
+
await this.io.writeText(entry.target, entry.content ?? '');
|
|
2947
2994
|
}
|
|
2948
2995
|
}
|
|
2949
2996
|
catch (error) {
|
|
2950
|
-
// A1-15 (v18): a post-rename dir-fsync failure means the bytes DID
|
|
2951
|
-
// land — rolling back would delete a visible write, and the audit/
|
|
2952
|
-
// event below must still run. Record it and keep going.
|
|
2953
2997
|
if (!isCommittedOnly(error))
|
|
2954
2998
|
throw error;
|
|
2955
2999
|
durabilityWarning = error instanceof Error ? error.message : String(error);
|
|
@@ -2958,11 +3002,6 @@ export class SkillLibrary {
|
|
|
2958
3002
|
}
|
|
2959
3003
|
}
|
|
2960
3004
|
catch (error) {
|
|
2961
|
-
// V27 G8.3: the rollback is best-effort (the .backups snapshot is the
|
|
2962
|
-
// recovery path when the caller took one), so the result must not claim a
|
|
2963
|
-
// rollback that did not happen. Track which targets failed to roll back
|
|
2964
|
-
// and name them; a silent `.catch` made the message a promise the code
|
|
2965
|
-
// never made.
|
|
2966
3005
|
const stuck = [];
|
|
2967
3006
|
for (const entry of written.reverse()) {
|
|
2968
3007
|
try {
|
|
@@ -2977,56 +3016,41 @@ export class SkillLibrary {
|
|
|
2977
3016
|
}
|
|
2978
3017
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2979
3018
|
if (stuck.length === 0)
|
|
2980
|
-
return { ok: false, message:
|
|
2981
|
-
return { ok: false, message:
|
|
2982
|
-
}
|
|
2983
|
-
// A7: both sides come from the KERNEL's own pre-read of the SKILL.md target — the baseline the
|
|
2984
|
-
// CAS write compared against and the bytes the rollback restores — instead of the earlier `md`
|
|
2985
|
-
// read, which names a state the commit did not necessarily replace. (In every current caller the
|
|
2986
|
-
// CAS refuses a drift between the two reads, so this is the same value on every committing run.)
|
|
2987
|
-
// G3a: ONE tree change is ONE unit — every path it wrote is audited under a shared unit id.
|
|
2988
|
-
// The old form picked only the SKILL.md landing, so a support file the same change moved got
|
|
2989
|
-
// no version entry at all and the undo could not restore the file set as a unit.
|
|
3019
|
+
return { ok: false, message: '目录树变更失败,已回滚:' + reason };
|
|
3020
|
+
return { ok: false, message: '目录树变更失败:' + reason + '。回滚无法恢复 ' + stuck.join(', ') + ':可恢复性由当前快照决定:' + (await this.snapshotRecoveryAvailability()) + ' —— 若上面不是「最新快照」,请重新应用这次变更。' };
|
|
3021
|
+
}
|
|
2990
3022
|
const unitId = plan.unitId ?? 'u-' + Date.now().toString(36) + '-' + (nextUnitSeq += 1).toString(36);
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
?
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
// A22: previous is string | null (a read failure rejects the whole plan), so the old
|
|
3017
|
-
// `=== undefined ? md : previous` was an unreachable branch -- the type is the truth.
|
|
3018
|
-
const auditedBefore = land.previous;
|
|
3019
|
-
const auditedAfter = land.content;
|
|
3020
|
-
// G5: the plan already carries the write face (`origin`) — put it in the ledger; G3a adds the unit.
|
|
3021
|
-
await this.audit(name, plan.auditAction, auditedBefore, auditedAfter, plan.auditSummary, rel, unitId, unitHead, plan.origin);
|
|
3022
|
-
}
|
|
3023
|
-
this.notifyMutation({ action: plan.eventAction, name, skillDir: dir });
|
|
3023
|
+
const defaultHead = {
|
|
3024
|
+
moves: landing
|
|
3025
|
+
.filter(entry => basename(entry.target) !== 'SKILL.md')
|
|
3026
|
+
.map(entry => ({
|
|
3027
|
+
path: entry.rel,
|
|
3028
|
+
kind: entry.kind === 'delete' ? 'removed' : entry.previous === null ? 'created' : 'moved',
|
|
3029
|
+
...(entry.previous === null ? {} : { beforeRef: contentHash(entry.previous) }),
|
|
3030
|
+
...(entry.kind === 'delete' || entry.content === null ? {} : { afterRef: contentHash(entry.content) }),
|
|
3031
|
+
})),
|
|
3032
|
+
sources: roots.map(root => ({ skill: root.name })),
|
|
3033
|
+
};
|
|
3034
|
+
const affected = new Set();
|
|
3035
|
+
for (let index = 0; index < landing.length; index += 1) {
|
|
3036
|
+
const entry = landing[index];
|
|
3037
|
+
if (entry === undefined)
|
|
3038
|
+
continue;
|
|
3039
|
+
await this.audit(entry.skill, plan.auditAction, entry.previous, entry.kind === 'delete' ? null : entry.content, plan.auditSummary, entry.rel === 'SKILL.md' ? undefined : entry.rel, unitId, index === 0 ? (plan.unitHead ?? defaultHead) : undefined, plan.origin);
|
|
3040
|
+
affected.add(entry.skill);
|
|
3041
|
+
}
|
|
3042
|
+
for (const skill of affected) {
|
|
3043
|
+
const dir = roots.find(root => root.name === skill)?.dir;
|
|
3044
|
+
if (dir !== undefined)
|
|
3045
|
+
this.notifyMutation({ action: plan.eventAction, name: skill, skillDir: dir });
|
|
3046
|
+
}
|
|
3047
|
+
const primaryDir = roots.find(root => root.name === primaryName)?.dir ?? roots[0]?.dir ?? '';
|
|
3024
3048
|
return {
|
|
3025
3049
|
ok: true,
|
|
3026
3050
|
message: durabilityWarning === ''
|
|
3027
|
-
?
|
|
3028
|
-
:
|
|
3029
|
-
path:
|
|
3051
|
+
? plan.eventAction + ' "' + primaryName + '" succeeded.'
|
|
3052
|
+
: plan.eventAction + ' "' + primaryName + '" succeeded (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:' + durabilityWarning + ')',
|
|
3053
|
+
path: primaryDir,
|
|
3030
3054
|
};
|
|
3031
3055
|
}
|
|
3032
3056
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support-directory references in a markdown body (009 kernel): references/…,
|
|
3
|
+
* templates/…, scripts/…, assets/… relative links — any extension and nested
|
|
4
|
+
* paths. Pure: who checks them and what the verdict is belongs to the caller.
|
|
5
|
+
* `..` traversal is a different defect class (path validation owns it) and is
|
|
6
|
+
* not a support link.
|
|
7
|
+
*/
|
|
8
|
+
export declare function supportRefs(content: string): string[];
|
|
9
|
+
/**
|
|
10
|
+
* References that leave the skill root: parent traversals or absolute support paths.
|
|
11
|
+
* Extract must not silently treat these as relative support files; the caller reports
|
|
12
|
+
* them (bounded external copy is a separate, explicit policy).
|
|
13
|
+
* @param content - markdown body.
|
|
14
|
+
* @returns external-looking support references, de-duplicated in first-seen order.
|
|
15
|
+
*/
|
|
16
|
+
export declare function externalRefs(content: string): string[];
|
|
17
|
+
//# sourceMappingURL=support-refs.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support-directory references in a markdown body (009 kernel): references/…,
|
|
3
|
+
* templates/…, scripts/…, assets/… relative links — any extension and nested
|
|
4
|
+
* paths. Pure: who checks them and what the verdict is belongs to the caller.
|
|
5
|
+
* `..` traversal is a different defect class (path validation owns it) and is
|
|
6
|
+
* not a support link.
|
|
7
|
+
*/
|
|
8
|
+
export function supportRefs(content) {
|
|
9
|
+
const refs = [];
|
|
10
|
+
for (const match of content.matchAll(/(?<![A-Za-z0-9._/-])(?:references|templates|scripts|assets)\/[A-Za-z0-9._/-]+/g)) {
|
|
11
|
+
const ref = match[0];
|
|
12
|
+
if (ref.includes('..'))
|
|
13
|
+
continue;
|
|
14
|
+
refs.push(ref);
|
|
15
|
+
}
|
|
16
|
+
return refs;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* References that leave the skill root: parent traversals or absolute support paths.
|
|
20
|
+
* Extract must not silently treat these as relative support files; the caller reports
|
|
21
|
+
* them (bounded external copy is a separate, explicit policy).
|
|
22
|
+
* @param content - markdown body.
|
|
23
|
+
* @returns external-looking support references, de-duplicated in first-seen order.
|
|
24
|
+
*/
|
|
25
|
+
export function externalRefs(content) {
|
|
26
|
+
const refs = [];
|
|
27
|
+
for (const match of content.matchAll(/(?:^|[\s("'`])((?:\.\.\/)+[A-Za-z0-9._/-]+|\/[A-Za-z0-9._/-]+)/g)) {
|
|
28
|
+
const ref = match[1]?.replace(/[.,;:]+$/, '');
|
|
29
|
+
if (ref === undefined || ref.length === 0 || refs.includes(ref))
|
|
30
|
+
continue;
|
|
31
|
+
refs.push(ref);
|
|
32
|
+
}
|
|
33
|
+
return refs;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=support-refs.js.map
|
package/package.json
CHANGED