@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.
@@ -0,0 +1,19 @@
1
+ import type { ExternalSupportFile } from './extract-plan.ts';
2
+ /**
3
+ * Read and annotate external support files under one explicit root. The caller owns
4
+ * the root choice; this module only enforces containment, target-path safety, size
5
+ * limits and determinism.
6
+ * @param body - selected source markdown body.
7
+ * @param sourceDir - source skill directory.
8
+ * @param allowedRoot - canonical root external files must stay under.
9
+ * @param targetFiles - existing target support paths.
10
+ * @returns resolved external entries, or a refusal.
11
+ */
12
+ export declare function readExternalSupport(body: string, sourceDir: string, allowedRoot: string, targetFiles: readonly string[]): Promise<{
13
+ ok: true;
14
+ files: ExternalSupportFile[];
15
+ } | {
16
+ ok: false;
17
+ message: string;
18
+ }>;
19
+ //# sourceMappingURL=external-support.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, realpath, stat } from 'node:fs/promises';
3
+ import { basename, isAbsolute, relative, resolve } from 'node:path';
4
+ import { externalRefs } from "./support-refs.js";
5
+ const MAX_FILE_BYTES = 1024 * 1024;
6
+ const MAX_TOTAL_BYTES = 4 * 1024 * 1024;
7
+ function targetPathFor(ref) {
8
+ let value = ref.replace(/^\/+/, '');
9
+ while (value.startsWith('../'))
10
+ value = value.slice(3);
11
+ const support = value.match(/(?:^|\/)(references|templates|scripts|assets)\/(.+)$/);
12
+ if (support !== null)
13
+ return support[1] + '/' + support[2];
14
+ return 'references/' + basename(value);
15
+ }
16
+ function annotate(content, originalPath) {
17
+ const note = 'external source: ' + originalPath;
18
+ const extension = originalPath.slice(originalPath.lastIndexOf('.')).toLowerCase();
19
+ if (extension === '.md')
20
+ return '<!-- ' + note + ' -->' + String.fromCharCode(10) + String.fromCharCode(10) + content;
21
+ const prefix = extension === '.py' || extension === '.sh' || extension === '.rb' ? '# ' : '// ';
22
+ if (extension === '.js' || extension === '.mjs' || extension === '.cjs' || extension === '.ts' || extension === '.tsx' || extension === '.py' || extension === '.sh' || extension === '.rb') {
23
+ if (content.startsWith('#!')) {
24
+ const end = content.indexOf(String.fromCharCode(10));
25
+ if (end >= 0)
26
+ return content.slice(0, end + 1) + prefix + note + String.fromCharCode(10) + content.slice(end + 1);
27
+ }
28
+ return prefix + note + String.fromCharCode(10) + content;
29
+ }
30
+ return content;
31
+ }
32
+ function inside(root, candidate) {
33
+ const rel = relative(root, candidate);
34
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
35
+ }
36
+ /**
37
+ * Read and annotate external support files under one explicit root. The caller owns
38
+ * the root choice; this module only enforces containment, target-path safety, size
39
+ * limits and determinism.
40
+ * @param body - selected source markdown body.
41
+ * @param sourceDir - source skill directory.
42
+ * @param allowedRoot - canonical root external files must stay under.
43
+ * @param targetFiles - existing target support paths.
44
+ * @returns resolved external entries, or a refusal.
45
+ */
46
+ export async function readExternalSupport(body, sourceDir, allowedRoot, targetFiles) {
47
+ const refs = externalRefs(body);
48
+ if (refs.length === 0)
49
+ return { ok: true, files: [] };
50
+ let root;
51
+ try {
52
+ root = await realpath(allowedRoot);
53
+ }
54
+ catch {
55
+ return { ok: false, message: 'external support 根目录不可读:' + allowedRoot };
56
+ }
57
+ const files = [];
58
+ const targets = new Set(targetFiles);
59
+ let total = 0;
60
+ for (const ref of refs) {
61
+ const candidate = isAbsolute(ref) ? resolve(ref) : resolve(sourceDir, ref);
62
+ let canonical;
63
+ try {
64
+ canonical = await realpath(candidate);
65
+ }
66
+ catch {
67
+ return { ok: false, message: 'external support 文件不存在:' + ref };
68
+ }
69
+ if (!inside(root, canonical))
70
+ return { ok: false, message: 'external support 越界:' + ref };
71
+ const info = await stat(canonical);
72
+ if (!info.isFile())
73
+ return { ok: false, message: 'external support 不是普通文件:' + ref };
74
+ if (info.size > MAX_FILE_BYTES)
75
+ return { ok: false, message: 'external support 超过单文件上限:' + ref };
76
+ total += info.size;
77
+ if (total > MAX_TOTAL_BYTES)
78
+ return { ok: false, message: 'external support 超过总上限:' + ref };
79
+ const targetPath = targetPathFor(ref);
80
+ if (targets.has(targetPath))
81
+ return { ok: false, message: 'external support 目标冲突:' + targetPath };
82
+ targets.add(targetPath);
83
+ const raw = await readFile(canonical, 'utf8');
84
+ const annotated = annotate(raw, canonical);
85
+ files.push({
86
+ ref,
87
+ originalPath: canonical,
88
+ targetPath,
89
+ content: annotated,
90
+ sha256: createHash('sha256').update(annotated).digest('hex'),
91
+ provenance: 'external:' + canonical,
92
+ });
93
+ }
94
+ return { ok: true, files };
95
+ }
96
+ //# sourceMappingURL=external-support.js.map
@@ -0,0 +1,127 @@
1
+ import { type SectionSelectionRequest } from './section-selection.ts';
2
+ /** Semantic outcome carried by a plan; the agent decides it, this module records it. */
3
+ export type ExtractOutcome = 'extract' | 'mixed' | 'owner-gap';
4
+ /** Owner coverage supplied by the owner resolver; a missing owner is owner-gap, not a guess. */
5
+ export interface OwnerDecision {
6
+ mode: 'covered' | 'owner-gap';
7
+ name?: string;
8
+ neededOwner?: string;
9
+ triggerCovers: boolean;
10
+ outcomeCovers: boolean;
11
+ reason: string;
12
+ }
13
+ export type ExtractPartRequest = SectionSelectionRequest;
14
+ export interface ExtractPart {
15
+ heading: string;
16
+ anchor: {
17
+ heading: string;
18
+ start?: string;
19
+ end?: string;
20
+ };
21
+ sourceText: string;
22
+ movedText: string;
23
+ bytes: number;
24
+ sha256: string;
25
+ }
26
+ export interface ExternalSupportFile {
27
+ ref: string;
28
+ originalPath: string;
29
+ targetPath: string;
30
+ content: string;
31
+ sha256: string;
32
+ provenance: string;
33
+ }
34
+ export interface SupportClosure {
35
+ exclusive: Array<{
36
+ from: string;
37
+ to: string;
38
+ }>;
39
+ shared: Array<{
40
+ from: string;
41
+ to: string;
42
+ provenance: string;
43
+ }>;
44
+ external: Array<{
45
+ from: string;
46
+ to: string;
47
+ provenance: string;
48
+ originalPath: string;
49
+ content?: string;
50
+ sha256?: string;
51
+ }>;
52
+ unresolved: string[];
53
+ dangling: string[];
54
+ }
55
+ export interface ExtractAnchors {
56
+ sourceMdSha256: string;
57
+ targetMdSha256: string;
58
+ }
59
+ export interface ExtractPlanDraft {
60
+ source: string;
61
+ target: {
62
+ name: string;
63
+ exists: boolean;
64
+ };
65
+ outcome: ExtractOutcome;
66
+ parts: ExtractPart[];
67
+ residual: string;
68
+ pointer: string;
69
+ owner: OwnerDecision;
70
+ support: SupportClosure;
71
+ warnings: string[];
72
+ /** Plan-time full-document hashes; the write adapter refuses when either drifts. */
73
+ anchors?: ExtractAnchors;
74
+ }
75
+ export interface PlanExtractInput {
76
+ body: string;
77
+ sourceName: string;
78
+ targetName: string;
79
+ targetExists: boolean;
80
+ outcome: ExtractOutcome;
81
+ owner: OwnerDecision;
82
+ requests: readonly ExtractPartRequest[];
83
+ sourceFiles: readonly string[];
84
+ targetFiles: readonly string[];
85
+ /** Bounded external files the caller read and resolved under its allowed root. */
86
+ external?: readonly ExternalSupportFile[];
87
+ /** Optional plan-time full-document hashes, threaded by the tool/CLI caller. */
88
+ anchors?: ExtractAnchors;
89
+ }
90
+ export type ExtractPlanResult = {
91
+ ok: true;
92
+ plan: ExtractPlanDraft;
93
+ } | {
94
+ ok: false;
95
+ code: string;
96
+ message: string;
97
+ };
98
+ /**
99
+ * Build a pure extract plan. No IO, no writes: selection, residual, support
100
+ * closure and the moved text are computed from the supplied bodies/file lists.
101
+ * @param input - source body, target identity, owner decision and support lists.
102
+ * @returns a plan, or a refusal with a stable code.
103
+ */
104
+ export declare function planExtract(input: PlanExtractInput): ExtractPlanResult;
105
+ export interface ExtractProjection {
106
+ sourceAfter: string;
107
+ targetAfter: string;
108
+ }
109
+ /**
110
+ * Project one plan onto the current source/target SKILL.md bytes.
111
+ *
112
+ * This is the one place that composes frontmatter + residual + target additions;
113
+ * preview and the write adapter both call it so they cannot advertise different
114
+ * bytes. Pure: no IO.
115
+ * @param plan - plan from planExtract.
116
+ * @param sourceMd - current source SKILL.md.
117
+ * @param targetMd - current target SKILL.md.
118
+ * @returns both next documents, or the refusal that makes projection impossible.
119
+ */
120
+ export declare function projectExtractBodies(plan: ExtractPlanDraft, sourceMd: string, targetMd: string): {
121
+ ok: true;
122
+ projection: ExtractProjection;
123
+ } | {
124
+ ok: false;
125
+ message: string;
126
+ };
127
+ //# sourceMappingURL=extract-plan.d.ts.map
@@ -0,0 +1,232 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { applyReferenceRewrite, planReferenceRewrite, planRehoming } from "./reference-rewrite.js";
3
+ import { selectSections } from "./section-selection.js";
4
+ import { externalRefs, supportRefs } from "./support-refs.js";
5
+ import { frontmatterBlock } from "./frontmatter.js";
6
+ const LF = String.fromCharCode(10);
7
+ function sha256(text) {
8
+ return createHash('sha256').update(text).digest('hex');
9
+ }
10
+ function unique(values) {
11
+ return [...new Set(values)];
12
+ }
13
+ function computeSupport(parts, residual, sourceName, sourceFiles, targetFiles, externalFiles) {
14
+ const selectedText = parts.map(part => part.sourceText).join(LF + LF);
15
+ const selectedRefs = unique(supportRefs(selectedText));
16
+ const residualRefs = new Set(supportRefs(residual));
17
+ const unresolved = selectedRefs.filter(ref => !sourceFiles.includes(ref));
18
+ if (unresolved.length > 0)
19
+ return { error: 'support-unresolved: ' + unresolved.join(', ') };
20
+ const candidates = selectedRefs.filter(ref => sourceFiles.includes(ref));
21
+ const rehome = planRehoming(candidates, targetFiles, sourceName);
22
+ const moves = rehome.moves;
23
+ const byFrom = new Map(moves.map(move => [move.from, move.to]));
24
+ const exclusive = [];
25
+ const shared = [];
26
+ for (const from of candidates) {
27
+ const to = byFrom.get(from);
28
+ if (to === undefined)
29
+ continue;
30
+ if (residualRefs.has(from))
31
+ shared.push({ from, to, provenance: sourceName + ':' + from });
32
+ else
33
+ exclusive.push({ from, to });
34
+ }
35
+ const targetFilesWithMoves = unique([...targetFiles, ...moves.map(move => move.to)]);
36
+ const external = unique(externalRefs(selectedText));
37
+ const suppliedRefs = new Set(externalFiles.map(file => file.ref));
38
+ const missingExternal = external.filter(ref => !suppliedRefs.has(ref));
39
+ const warnings = [];
40
+ if (missingExternal.length > 0)
41
+ warnings.push('external-support-not-copied: ' + missingExternal.join(', '));
42
+ const externalClosure = [];
43
+ for (const file of externalFiles) {
44
+ if (!external.includes(file.ref))
45
+ return { error: 'external-ref-not-selected: ' + file.ref };
46
+ if (file.targetPath.startsWith('/') || file.targetPath.includes('..') || !/^(?:references|templates|scripts|assets)\//.test(file.targetPath)) {
47
+ return { error: 'external-target-unsafe: ' + file.targetPath };
48
+ }
49
+ if (targetFilesWithMoves.includes(file.targetPath))
50
+ return { error: 'external-collision: ' + file.targetPath };
51
+ externalClosure.push({
52
+ from: file.ref,
53
+ to: file.targetPath,
54
+ provenance: file.provenance,
55
+ originalPath: file.originalPath,
56
+ content: file.content,
57
+ sha256: file.sha256,
58
+ });
59
+ }
60
+ const rewritten = new Map();
61
+ for (const part of parts) {
62
+ const partRefs = supportRefs(part.sourceText);
63
+ const rewrite = planReferenceRewrite({
64
+ content: part.sourceText,
65
+ files: sourceFiles,
66
+ moves: moves.filter(move => partRefs.includes(move.from)),
67
+ targetFiles: targetFilesWithMoves,
68
+ });
69
+ rewritten.set(part.heading, applyReferenceRewrite(part.sourceText, rewrite.edits));
70
+ warnings.push(...rewrite.unresolved.map(ref => 'support-unresolved: ' + ref));
71
+ warnings.push(...rewrite.residualDangling.map(ref => 'support-dangling: ' + ref));
72
+ }
73
+ for (const part of parts) {
74
+ let text = rewritten.get(part.heading) ?? part.sourceText;
75
+ for (const file of externalClosure)
76
+ text = text.split(file.from).join(file.to);
77
+ part.movedText = text;
78
+ }
79
+ const exclusiveSet = new Set(exclusive.map(entry => entry.from));
80
+ const parentExclusive = supportRefs(residual).filter(ref => exclusiveSet.has(ref));
81
+ if (parentExclusive.length > 0)
82
+ return { error: 'support-exclusive-referenced-by-parent: ' + parentExclusive.join(', ') };
83
+ const dangling = unique(warnings.filter(item => item.startsWith('support-dangling: ')).map(item => item.slice('support-dangling: '.length)));
84
+ return {
85
+ support: {
86
+ exclusive,
87
+ shared,
88
+ external: externalClosure,
89
+ unresolved: warnings.filter(item => item.startsWith('support-unresolved: ')),
90
+ dangling,
91
+ },
92
+ warnings,
93
+ };
94
+ }
95
+ /**
96
+ * Build a pure extract plan. No IO, no writes: selection, residual, support
97
+ * closure and the moved text are computed from the supplied bodies/file lists.
98
+ * @param input - source body, target identity, owner decision and support lists.
99
+ * @returns a plan, or a refusal with a stable code.
100
+ */
101
+ export function planExtract(input) {
102
+ if (input.outcome === 'owner-gap') {
103
+ if (input.owner.mode !== 'owner-gap')
104
+ return { ok: false, code: 'owner-gap-mismatch', message: 'outcome owner-gap requires an owner-gap decision' };
105
+ if (input.owner.neededOwner === undefined || input.owner.neededOwner.trim() === '') {
106
+ return { ok: false, code: 'owner-gap-needed-owner', message: 'owner-gap requires neededOwner' };
107
+ }
108
+ }
109
+ else {
110
+ if (input.owner.mode !== 'covered' || !input.owner.triggerCovers || !input.owner.outcomeCovers) {
111
+ return { ok: false, code: 'owner-not-covered', message: 'extract/mixed requires a covering owner' };
112
+ }
113
+ if (!input.targetExists)
114
+ return { ok: false, code: 'target-missing', message: 'covered owner target does not exist' };
115
+ }
116
+ const selection = selectSections(input.body, input.requests);
117
+ if (!selection.ok)
118
+ return { ok: false, code: selection.code, message: selection.message };
119
+ if (selection.sections.length === 0)
120
+ return { ok: false, code: 'part-empty', message: 'at least one part is required' };
121
+ const parts = selection.sections.map(section => ({
122
+ heading: section.heading,
123
+ anchor: {
124
+ heading: section.heading,
125
+ ...(section.anchorStart === undefined ? {} : { start: section.anchorStart }),
126
+ ...(section.anchorEnd === undefined ? {} : { end: section.anchorEnd }),
127
+ },
128
+ sourceText: section.text,
129
+ movedText: section.text,
130
+ bytes: Buffer.byteLength(section.text, 'utf8'),
131
+ sha256: sha256(section.text),
132
+ }));
133
+ const ownerName = input.owner.mode === 'covered' ? (input.owner.name ?? input.targetName) : (input.owner.neededOwner ?? input.targetName);
134
+ const pointer = 'This capability now lives in ' + ownerName + '; project-specific facts stay here.';
135
+ const ordered = [...selection.sections].sort((a, b) => a.start - b.start);
136
+ const first = ordered[0];
137
+ if (first === undefined)
138
+ return { ok: false, code: 'part-empty', message: 'at least one part is required' };
139
+ const byStart = new Map(ordered.map(section => [section.start, section]));
140
+ const lines = input.body.split(LF);
141
+ const rebuilt = [];
142
+ for (let index = 0; index < lines.length; index += 1) {
143
+ const section = byStart.get(index);
144
+ if (section !== undefined) {
145
+ if (index === first.start)
146
+ rebuilt.push(pointer);
147
+ index = section.end - 1;
148
+ }
149
+ else {
150
+ rebuilt.push(lines[index] ?? '');
151
+ }
152
+ }
153
+ const residual = rebuilt.join(LF);
154
+ if (residual.trim().length === 0)
155
+ return { ok: false, code: 'parent-empty', message: 'parent residual is empty' };
156
+ const substantive = residual.split(LF).filter(line => line.trim() !== '' && line !== pointer && !line.trimStart().startsWith('#'));
157
+ if (substantive.length === 0)
158
+ return { ok: false, code: 'parent-hollow-heading', message: 'parent residual has no substantive content' };
159
+ if (input.outcome === 'owner-gap') {
160
+ return {
161
+ ok: true,
162
+ plan: {
163
+ source: input.sourceName,
164
+ target: { name: input.targetName, exists: input.targetExists },
165
+ outcome: input.outcome,
166
+ parts,
167
+ residual,
168
+ pointer,
169
+ owner: input.owner,
170
+ support: { exclusive: [], shared: [], external: [], unresolved: [], dangling: [] },
171
+ warnings: [],
172
+ ...(input.anchors === undefined ? {} : { anchors: input.anchors }),
173
+ },
174
+ };
175
+ }
176
+ const supportResult = computeSupport(parts, residual, input.sourceName, input.sourceFiles, input.targetFiles, input.external ?? []);
177
+ if ('error' in supportResult) {
178
+ const code = supportResult.error.split(':')[0] ?? 'support-invalid';
179
+ return { ok: false, code, message: supportResult.error };
180
+ }
181
+ if (supportResult.support.dangling.length > 0) {
182
+ return { ok: false, code: 'support-dangling', message: 'support-dangling: ' + supportResult.support.dangling.join(', ') };
183
+ }
184
+ return {
185
+ ok: true,
186
+ plan: {
187
+ source: input.sourceName,
188
+ target: { name: input.targetName, exists: input.targetExists },
189
+ outcome: input.outcome,
190
+ parts,
191
+ residual,
192
+ pointer,
193
+ owner: input.owner,
194
+ support: supportResult.support,
195
+ warnings: unique(supportResult.warnings),
196
+ ...(input.anchors === undefined ? {} : { anchors: input.anchors }),
197
+ },
198
+ };
199
+ }
200
+ /**
201
+ * Project one plan onto the current source/target SKILL.md bytes.
202
+ *
203
+ * This is the one place that composes frontmatter + residual + target additions;
204
+ * preview and the write adapter both call it so they cannot advertise different
205
+ * bytes. Pure: no IO.
206
+ * @param plan - plan from planExtract.
207
+ * @param sourceMd - current source SKILL.md.
208
+ * @param targetMd - current target SKILL.md.
209
+ * @returns both next documents, or the refusal that makes projection impossible.
210
+ */
211
+ export function projectExtractBodies(plan, sourceMd, targetMd) {
212
+ const sourceBlock = frontmatterBlock(sourceMd);
213
+ const targetBlock = frontmatterBlock(targetMd);
214
+ if (!sourceBlock)
215
+ return { ok: false, message: 'source 没有有效 frontmatter' };
216
+ if (!targetBlock)
217
+ return { ok: false, message: 'target 没有有效 frontmatter' };
218
+ const sourceHeader = sourceBlock.lines.slice(0, sourceBlock.end + 1).join(LF);
219
+ const targetNormalized = targetMd.replace(/\r\n/g, LF);
220
+ const targetLines = targetNormalized.split(LF);
221
+ for (const part of plan.parts) {
222
+ if (targetLines.some(line => line.trim() === '## ' + part.heading))
223
+ return { ok: false, message: 'target 已有同名 heading「' + part.heading + '」。' };
224
+ }
225
+ const additions = plan.parts.map(part => part.movedText).join(LF + LF);
226
+ const targetRaw = targetNormalized.trimEnd() + LF + LF + additions + LF;
227
+ const targetAfter = targetBlock.nl === '\r\n' ? targetRaw.replace(/\n/g, '\r\n') : targetRaw;
228
+ const sourceRaw = (sourceHeader + plan.residual).replace(/\r\n/g, LF);
229
+ const sourceAfter = sourceBlock.nl === '\r\n' ? sourceRaw.replace(/\n/g, '\r\n') : sourceRaw;
230
+ return { ok: true, projection: { sourceAfter, targetAfter } };
231
+ }
232
+ //# sourceMappingURL=extract-plan.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The frozen semantic prompt for skill extraction. One constant, consumed by
3
+ * explicit extract agents and tests; it is deliberately not part of the review
4
+ * prompt bundle, and no review path injects it.
5
+ */
6
+ export declare const EXTRACT_PROMPT_VERSION = "v1";
7
+ export declare const EXTRACT_PROMPT_V1 = "Inspect the source skill, its linked support files, and the candidate owners\nin the library. A part of the skill (one or more sections) is a separate\ncapability when it has its own trigger and outcome, can be used without the\nparent context, and leaves the parent coherent. If it is separate and a\nclass-level owner covers it, extract that part. If no owner covers it, report\nthe owner gap and name the class-level owner that would be needed. If it mixes\na reusable capability with parent-specific facts, return mixed and name only\nthe reusable part. Otherwise keep it. Keep, extract, mixed, and owner-gap are\nall valid outcomes; no split is required.";
8
+ //# sourceMappingURL=extract-prompt.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The frozen semantic prompt for skill extraction. One constant, consumed by
3
+ * explicit extract agents and tests; it is deliberately not part of the review
4
+ * prompt bundle, and no review path injects it.
5
+ */
6
+ export const EXTRACT_PROMPT_VERSION = 'v1';
7
+ export const EXTRACT_PROMPT_V1 = `Inspect the source skill, its linked support files, and the candidate owners
8
+ in the library. A part of the skill (one or more sections) is a separate
9
+ capability when it has its own trigger and outcome, can be used without the
10
+ parent context, and leaves the parent coherent. If it is separate and a
11
+ class-level owner covers it, extract that part. If no owner covers it, report
12
+ the owner gap and name the class-level owner that would be needed. If it mixes
13
+ a reusable capability with parent-specific facts, return mixed and name only
14
+ the reusable part. Otherwise keep it. Keep, extract, mixed, and owner-gap are
15
+ all valid outcomes; no split is required.`;
16
+ //# sourceMappingURL=extract-prompt.js.map
@@ -41,6 +41,14 @@ export * from './package-identity.ts';
41
41
  export * from './write-authority.ts';
42
42
  export * from './write-gates.ts';
43
43
  export * from './reference-rewrite.ts';
44
+ export * from './support-refs.ts';
45
+ export * from './section-selection.ts';
46
+ export * from './extract-plan.ts';
47
+ export * from './owner-resolver.ts';
48
+ export * from './proposal-view.ts';
49
+ export * from './proposal-report.ts';
50
+ export * from './external-support.ts';
51
+ export * from './extract-prompt.ts';
44
52
  export * from './cost.ts';
45
53
  export * from './skill-lint.ts';
46
54
  export * from './health-baseline.ts';
@@ -42,6 +42,14 @@ export * from "./package-identity.js";
42
42
  export * from "./write-authority.js";
43
43
  export * from "./write-gates.js";
44
44
  export * from "./reference-rewrite.js";
45
+ export * from "./support-refs.js";
46
+ export * from "./section-selection.js";
47
+ export * from "./extract-plan.js";
48
+ export * from "./owner-resolver.js";
49
+ export * from "./proposal-view.js";
50
+ export * from "./proposal-report.js";
51
+ export * from "./external-support.js";
52
+ export * from "./extract-prompt.js";
45
53
  export * from "./cost.js";
46
54
  export * from "./skill-lint.js";
47
55
  export * from "./health-baseline.js";
@@ -0,0 +1,40 @@
1
+ import type { SkillSummary } from './skill-store.ts';
2
+ /** One existing class-level owner candidate, with the routing facts a semantic adjudicator needs. */
3
+ export interface OwnerCandidate {
4
+ name: string;
5
+ description: string;
6
+ whenToUse?: string;
7
+ headings: string[];
8
+ protectedBy: string | null;
9
+ protectionUnknown: boolean;
10
+ managed: boolean;
11
+ }
12
+ export interface OwnerExclusion {
13
+ name: string;
14
+ reason: 'source' | 'unreadable';
15
+ }
16
+ export interface OwnerLookup {
17
+ source: string;
18
+ candidates: OwnerCandidate[];
19
+ excluded: OwnerExclusion[];
20
+ }
21
+ /**
22
+ * Project the current library listing into owner candidates.
23
+ *
24
+ * The source is excluded; a skill with an empty description is named in
25
+ * excluded rather than silently dropped. Protected candidates remain visible
26
+ * (so a caller can explain why it refused), but are marked for the writer's
27
+ * protection gate. Headings reuse h2Sections; no second heading scanner.
28
+ * @param source - skill being extracted from.
29
+ * @param summaries - SkillLibrary.list({ withContent: true, withFrontmatter: true }).
30
+ * @returns stable-name-ordered candidates plus explicit exclusions.
31
+ */
32
+ export declare function ownerCandidatesFromSummaries(source: string, summaries: readonly SkillSummary[]): OwnerLookup;
33
+ /**
34
+ * Find one named candidate in a lookup.
35
+ * @param lookup - projection from ownerCandidatesFromSummaries.
36
+ * @param name - exact skill name.
37
+ * @returns the candidate, or undefined when absent.
38
+ */
39
+ export declare function findOwnerCandidate(lookup: OwnerLookup, name: string): OwnerCandidate | undefined;
40
+ //# sourceMappingURL=owner-resolver.d.ts.map
@@ -0,0 +1,49 @@
1
+ import { h2Sections } from "./document-body.js";
2
+ /**
3
+ * Project the current library listing into owner candidates.
4
+ *
5
+ * The source is excluded; a skill with an empty description is named in
6
+ * excluded rather than silently dropped. Protected candidates remain visible
7
+ * (so a caller can explain why it refused), but are marked for the writer's
8
+ * protection gate. Headings reuse h2Sections; no second heading scanner.
9
+ * @param source - skill being extracted from.
10
+ * @param summaries - SkillLibrary.list({ withContent: true, withFrontmatter: true }).
11
+ * @returns stable-name-ordered candidates plus explicit exclusions.
12
+ */
13
+ export function ownerCandidatesFromSummaries(source, summaries) {
14
+ const normalized = source.trim();
15
+ const candidates = [];
16
+ const excluded = [];
17
+ for (const summary of summaries) {
18
+ if (summary.name === normalized) {
19
+ excluded.push({ name: summary.name, reason: 'source' });
20
+ continue;
21
+ }
22
+ if (summary.description.trim() === '') {
23
+ excluded.push({ name: summary.name, reason: 'unreadable' });
24
+ continue;
25
+ }
26
+ candidates.push({
27
+ name: summary.name,
28
+ description: summary.description,
29
+ ...(summary.whenToUse === undefined ? {} : { whenToUse: summary.whenToUse }),
30
+ headings: h2Sections(summary.content ?? '').map(section => section.title),
31
+ protectedBy: summary.protectedBy,
32
+ protectionUnknown: summary.protectionUnknown,
33
+ managed: summary.managed,
34
+ });
35
+ }
36
+ candidates.sort((left, right) => left.name.localeCompare(right.name));
37
+ excluded.sort((left, right) => left.name.localeCompare(right.name));
38
+ return { source: normalized, candidates, excluded };
39
+ }
40
+ /**
41
+ * Find one named candidate in a lookup.
42
+ * @param lookup - projection from ownerCandidatesFromSummaries.
43
+ * @param name - exact skill name.
44
+ * @returns the candidate, or undefined when absent.
45
+ */
46
+ export function findOwnerCandidate(lookup, name) {
47
+ return lookup.candidates.find(candidate => candidate.name === name);
48
+ }
49
+ //# sourceMappingURL=owner-resolver.js.map
@@ -0,0 +1,23 @@
1
+ import type { EvolutionIoLike } from './io.ts';
2
+ import type { ProposalView } from './proposal-view.ts';
3
+ /** Directory for the one proposal lane; reports are retention-swept with the other run reports. */
4
+ export declare function proposalReportDir(): string;
5
+ /** Stable report filename for one proposal; the id may carry path separators. */
6
+ export declare function proposalReportName(proposal: ProposalView): string;
7
+ /**
8
+ * Persist one proposal as a report. Best-effort: a failed write never blocks the extract.
9
+ * @param io - evolution IO seam.
10
+ * @param proposal - proposal to persist.
11
+ * @param dir - reports directory (tests pass a temp dir).
12
+ * @returns the written path.
13
+ */
14
+ export declare function writeProposalReport(io: EvolutionIoLike, proposal: ProposalView, dir?: string): Promise<string>;
15
+ /**
16
+ * Read the newest proposals. Missing/unreadable reports are skipped, never guessed.
17
+ * @param io - evolution IO seam.
18
+ * @param dir - reports directory.
19
+ * @param limit - newest rows to return.
20
+ * @returns valid proposals, newest filename first.
21
+ */
22
+ export declare function readProposalReports(io: EvolutionIoLike, dir?: string, limit?: number): Promise<ProposalView[]>;
23
+ //# sourceMappingURL=proposal-report.d.ts.map