@lmzhen/dsh-evolution-core 0.30.31 → 0.30.33
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 +1087 -239
- 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/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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { isPresent, isUnknown, probeList, probeText } from "./probe.js";
|
|
3
|
+
import { evolutionHome } from "./state-store.js";
|
|
4
|
+
/** Directory for the one proposal lane; reports are retention-swept with the other run reports. */
|
|
5
|
+
export function proposalReportDir() {
|
|
6
|
+
return join(evolutionHome(), 'reports');
|
|
7
|
+
}
|
|
8
|
+
/** Stable report filename for one proposal; the id may carry path separators. */
|
|
9
|
+
export function proposalReportName(proposal) {
|
|
10
|
+
return 'extract-proposal-' + encodeURIComponent(proposal.id) + '.json';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Persist one proposal as a report. Best-effort: a failed write never blocks the extract.
|
|
14
|
+
* @param io - evolution IO seam.
|
|
15
|
+
* @param proposal - proposal to persist.
|
|
16
|
+
* @param dir - reports directory (tests pass a temp dir).
|
|
17
|
+
* @returns the written path.
|
|
18
|
+
*/
|
|
19
|
+
export async function writeProposalReport(io, proposal, dir = proposalReportDir()) {
|
|
20
|
+
const path = join(dir, proposalReportName(proposal));
|
|
21
|
+
await io.writeText(path, JSON.stringify(proposal));
|
|
22
|
+
return path;
|
|
23
|
+
}
|
|
24
|
+
function isProposal(value) {
|
|
25
|
+
if (value === null || typeof value !== 'object')
|
|
26
|
+
return false;
|
|
27
|
+
const candidate = value;
|
|
28
|
+
return candidate.version === 1 && typeof candidate.id === 'string' && typeof candidate.source === 'string'
|
|
29
|
+
&& (candidate.outcome === 'keep' || candidate.outcome === 'extract' || candidate.outcome === 'mixed' || candidate.outcome === 'owner-gap');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the newest proposals. Missing/unreadable reports are skipped, never guessed.
|
|
33
|
+
* @param io - evolution IO seam.
|
|
34
|
+
* @param dir - reports directory.
|
|
35
|
+
* @param limit - newest rows to return.
|
|
36
|
+
* @returns valid proposals, newest filename first.
|
|
37
|
+
*/
|
|
38
|
+
export async function readProposalReports(io, dir = proposalReportDir(), limit = 10) {
|
|
39
|
+
const listed = await probeList(io, dir);
|
|
40
|
+
if (isUnknown(listed) || !isPresent(listed))
|
|
41
|
+
return [];
|
|
42
|
+
const names = listed.value.filter(name => name.startsWith('extract-proposal-') && name.endsWith('.json'));
|
|
43
|
+
names.sort((a, b) => b.localeCompare(a));
|
|
44
|
+
const proposals = [];
|
|
45
|
+
for (const name of names.slice(0, limit)) {
|
|
46
|
+
const raw = await probeText(io, join(dir, name));
|
|
47
|
+
if (!isPresent(raw))
|
|
48
|
+
continue;
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(raw.value);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (isProposal(parsed))
|
|
57
|
+
proposals.push(parsed);
|
|
58
|
+
}
|
|
59
|
+
return proposals;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=proposal-report.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ExtractOutcome, ExtractPlanDraft, OwnerDecision, SupportClosure } from './extract-plan.ts';
|
|
2
|
+
/** The semantic outcome a proposal may carry; keep is valid even though planExtract only plans a split. */
|
|
3
|
+
export type ProposalOutcome = ExtractOutcome | 'keep';
|
|
4
|
+
/** One semantic proposal carried to health/panel/audit surfaces. */
|
|
5
|
+
export interface ProposalView {
|
|
6
|
+
version: 1;
|
|
7
|
+
id: string;
|
|
8
|
+
source: string;
|
|
9
|
+
outcome: ProposalOutcome;
|
|
10
|
+
parts: Array<{
|
|
11
|
+
heading: string;
|
|
12
|
+
anchor: {
|
|
13
|
+
heading: string;
|
|
14
|
+
start?: string;
|
|
15
|
+
end?: string;
|
|
16
|
+
};
|
|
17
|
+
reason: string;
|
|
18
|
+
}>;
|
|
19
|
+
owner: OwnerDecision;
|
|
20
|
+
support?: SupportClosure;
|
|
21
|
+
warnings: string[];
|
|
22
|
+
provenance: {
|
|
23
|
+
promptVersion: 'v1';
|
|
24
|
+
runId: string;
|
|
25
|
+
at: number;
|
|
26
|
+
};
|
|
27
|
+
status: 'proposed' | 'previewed' | 'stale' | 'dismissed';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Project a pure ExtractPlan into the proposal record every read-only surface consumes.
|
|
31
|
+
*
|
|
32
|
+
* This is a projection, not a second evaluator: it keeps the plan's owner decision,
|
|
33
|
+
* support closure and warnings. It never writes and never invents coverage.
|
|
34
|
+
* @param plan - plan produced by planExtract.
|
|
35
|
+
* @param provenance - run id and timestamp for the proposing pass.
|
|
36
|
+
* @returns a proposal with status proposed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function proposalFromPlan(plan: ExtractPlanDraft, provenance: {
|
|
39
|
+
runId: string;
|
|
40
|
+
at?: number;
|
|
41
|
+
}): ProposalView;
|
|
42
|
+
//# sourceMappingURL=proposal-view.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project a pure ExtractPlan into the proposal record every read-only surface consumes.
|
|
3
|
+
*
|
|
4
|
+
* This is a projection, not a second evaluator: it keeps the plan's owner decision,
|
|
5
|
+
* support closure and warnings. It never writes and never invents coverage.
|
|
6
|
+
* @param plan - plan produced by planExtract.
|
|
7
|
+
* @param provenance - run id and timestamp for the proposing pass.
|
|
8
|
+
* @returns a proposal with status proposed.
|
|
9
|
+
*/
|
|
10
|
+
export function proposalFromPlan(plan, provenance) {
|
|
11
|
+
return {
|
|
12
|
+
version: 1,
|
|
13
|
+
id: 'extract-' + provenance.runId + '-' + plan.source,
|
|
14
|
+
source: plan.source,
|
|
15
|
+
outcome: plan.outcome,
|
|
16
|
+
parts: plan.parts.map(part => ({ heading: part.heading, anchor: part.anchor, reason: part.sourceText.slice(0, 160) })),
|
|
17
|
+
owner: plan.owner,
|
|
18
|
+
support: plan.support,
|
|
19
|
+
warnings: plan.warnings,
|
|
20
|
+
provenance: { promptVersion: 'v1', runId: provenance.runId, at: provenance.at ?? Date.now() },
|
|
21
|
+
status: 'proposed',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=proposal-view.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** One section/block selection request. Heading is exact; start/end select a content sub-block. */
|
|
2
|
+
export interface SectionSelectionRequest {
|
|
3
|
+
heading: string;
|
|
4
|
+
start?: string;
|
|
5
|
+
end?: string;
|
|
6
|
+
}
|
|
7
|
+
/** One selected range. Whole-H2 selections omit anchorStart/anchorEnd. */
|
|
8
|
+
export interface SelectedSection {
|
|
9
|
+
heading: string;
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
text: string;
|
|
13
|
+
requestIndex: number;
|
|
14
|
+
anchorStart?: string;
|
|
15
|
+
anchorEnd?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Selection result; failure messages preserve the restructure planner's wording. */
|
|
18
|
+
export type SectionSelectionResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
sections: SelectedSection[];
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
code: string;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Select whole H2 sections or exact content sub-blocks from one body.
|
|
28
|
+
*
|
|
29
|
+
* The H2 scan is document-body's h2Sections, so restructure and extract share
|
|
30
|
+
* one boundary rule. A start/end pair selects an inclusive, contiguous run of
|
|
31
|
+
* exact lines inside that heading; it cannot select the heading itself, overlap
|
|
32
|
+
* another request, or leave a selected range empty.
|
|
33
|
+
* @param body - markdown after frontmatter.
|
|
34
|
+
* @param requests - exact heading plus optional start/end line pairs.
|
|
35
|
+
* @returns selected ranges in request order, or the first refusal.
|
|
36
|
+
*/
|
|
37
|
+
export declare function selectSections(body: string, requests: readonly SectionSelectionRequest[]): SectionSelectionResult;
|
|
38
|
+
//# sourceMappingURL=section-selection.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { h2Sections } from "./document-body.js";
|
|
2
|
+
const LF = String.fromCharCode(10);
|
|
3
|
+
/**
|
|
4
|
+
* Select whole H2 sections or exact content sub-blocks from one body.
|
|
5
|
+
*
|
|
6
|
+
* The H2 scan is document-body's h2Sections, so restructure and extract share
|
|
7
|
+
* one boundary rule. A start/end pair selects an inclusive, contiguous run of
|
|
8
|
+
* exact lines inside that heading; it cannot select the heading itself, overlap
|
|
9
|
+
* another request, or leave a selected range empty.
|
|
10
|
+
* @param body - markdown after frontmatter.
|
|
11
|
+
* @param requests - exact heading plus optional start/end line pairs.
|
|
12
|
+
* @returns selected ranges in request order, or the first refusal.
|
|
13
|
+
*/
|
|
14
|
+
export function selectSections(body, requests) {
|
|
15
|
+
const lines = body.split(LF);
|
|
16
|
+
const sections = h2Sections(body);
|
|
17
|
+
const selected = [];
|
|
18
|
+
for (let requestIndex = 0; requestIndex < requests.length; requestIndex += 1) {
|
|
19
|
+
const request = requests[requestIndex];
|
|
20
|
+
if (request === undefined)
|
|
21
|
+
continue;
|
|
22
|
+
const wanted = request.heading.trim();
|
|
23
|
+
const matching = sections.filter(section => section.title === wanted);
|
|
24
|
+
if (matching.length === 0)
|
|
25
|
+
return { ok: false, code: 'heading-missing', message: 'no "## ' + wanted + '" heading in the body' };
|
|
26
|
+
if (matching.length > 1)
|
|
27
|
+
return { ok: false, code: 'heading-ambiguous', message: 'heading "## ' + wanted + '" appears ' + matching.length + ' times (ambiguous anchor)' };
|
|
28
|
+
const section = matching[0];
|
|
29
|
+
if (section === undefined)
|
|
30
|
+
return { ok: false, code: 'heading-missing', message: 'no "## ' + wanted + '" heading in the body' };
|
|
31
|
+
if (selected.some(entry => entry.start === section.start)) {
|
|
32
|
+
return { ok: false, code: 'heading-duplicate', message: 'heading "## ' + wanted + '" is moved twice' };
|
|
33
|
+
}
|
|
34
|
+
const partial = request.start !== undefined || request.end !== undefined;
|
|
35
|
+
if (!partial) {
|
|
36
|
+
if (section.end === section.start + 1)
|
|
37
|
+
return { ok: false, code: 'heading-empty', message: 'heading "## ' + wanted + '" has an empty section' };
|
|
38
|
+
selected.push({ heading: wanted, start: section.start, end: section.end, text: lines.slice(section.start, section.end).join(LF), requestIndex });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (request.start === undefined || request.end === undefined) {
|
|
42
|
+
return { ok: false, code: 'part-anchor-incomplete', message: 'heading "## ' + wanted + '" needs both start and end anchors' };
|
|
43
|
+
}
|
|
44
|
+
const startMatches = [];
|
|
45
|
+
const endMatches = [];
|
|
46
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
47
|
+
if (index <= section.start || index >= section.end)
|
|
48
|
+
continue;
|
|
49
|
+
if (lines[index] === request.start)
|
|
50
|
+
startMatches.push(index);
|
|
51
|
+
if (lines[index] === request.end)
|
|
52
|
+
endMatches.push(index);
|
|
53
|
+
}
|
|
54
|
+
if (startMatches.length !== 1)
|
|
55
|
+
return { ok: false, code: 'part-anchor-not-found', message: 'heading "## ' + wanted + '" start anchor must match exactly one content line' };
|
|
56
|
+
if (endMatches.length !== 1)
|
|
57
|
+
return { ok: false, code: 'part-anchor-not-found', message: 'heading "## ' + wanted + '" end anchor must match exactly one content line' };
|
|
58
|
+
const start = startMatches[0];
|
|
59
|
+
const end = endMatches[0];
|
|
60
|
+
if (start === undefined || end === undefined || start > end) {
|
|
61
|
+
return { ok: false, code: 'part-anchor-order', message: 'heading "## ' + wanted + '" start anchor is after end anchor' };
|
|
62
|
+
}
|
|
63
|
+
if (selected.some(entry => start < entry.end && end + 1 > entry.start)) {
|
|
64
|
+
return { ok: false, code: 'part-anchor-overlaps', message: 'heading "## ' + wanted + '" selection overlaps another selected block' };
|
|
65
|
+
}
|
|
66
|
+
selected.push({
|
|
67
|
+
heading: wanted,
|
|
68
|
+
start,
|
|
69
|
+
end: end + 1,
|
|
70
|
+
text: lines.slice(start, end + 1).join(LF),
|
|
71
|
+
requestIndex,
|
|
72
|
+
anchorStart: request.start,
|
|
73
|
+
anchorEnd: request.end,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, sections: selected };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=section-selection.js.map
|
|
@@ -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
|