@lmzhen/dsh-evolution-core 0.26.26 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/lib/composition/index.js +3 -0
- package/lib/composition-D8Y3-ROd.js +604 -0
- package/lib/ensure-MEYMj9jn.js +222 -0
- package/lib/errors-Br2-uUSK.js +57 -0
- package/lib/http-routes-BP-S3KV-.js +149 -0
- package/lib/http-routes.js +2 -0
- package/lib/index.js +66 -850
- package/lib/restrictor.js +37 -0
- package/lib/subagent/index.js +186 -0
- package/lib/types/citations.js +192 -0
- package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +1 -1
- package/lib/types/composition/composer.js +681 -0
- package/lib/types/composition/ensure.d.ts +10 -0
- package/lib/types/composition/ensure.js +95 -0
- package/lib/types/composition/index.d.ts +5 -0
- package/lib/types/composition/index.js +7 -0
- package/lib/types/composition/preset-ids.d.ts +17 -0
- package/lib/types/composition/preset-ids.js +21 -0
- package/lib/types/composition/restricted-preset.d.ts +36 -0
- package/lib/types/composition/restricted-preset.js +42 -0
- package/lib/types/constants.js +223 -0
- package/lib/types/cost.js +67 -0
- package/lib/types/curator.js +346 -0
- package/lib/types/diagnostics.d.ts +26 -0
- package/lib/types/diagnostics.js +23 -0
- package/lib/types/document-body.js +91 -0
- package/lib/types/drift-signals.js +319 -0
- package/lib/types/env.js +25 -0
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/errors.js +59 -0
- package/lib/types/events.js +16 -0
- package/lib/types/evidence.js +65 -0
- package/lib/types/evolution-events.js +479 -0
- package/lib/types/fold-untrusted.js +44 -0
- package/lib/types/frontmatter.js +623 -0
- package/lib/types/fuzzy-match.js +135 -0
- package/lib/types/gates.js +51 -0
- package/lib/types/guards.js +27 -0
- package/lib/types/health-baseline.js +63 -0
- package/lib/types/http-routes.d.ts +67 -35
- package/lib/types/http-routes.js +135 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/index.js +101 -0
- package/lib/types/instance-scope.js +69 -0
- package/lib/types/io.js +675 -0
- package/lib/types/learn-prompt.js +42 -0
- package/lib/types/limits.js +63 -0
- package/lib/types/live.js +107 -0
- package/lib/types/long-runs.js +96 -0
- package/lib/types/memory-store.d.ts +0 -16
- package/lib/types/memory-store.js +606 -0
- package/lib/types/mutations.js +95 -0
- package/lib/types/namespace-migration.js +156 -0
- package/lib/types/numeric.js +47 -0
- package/lib/types/opt-in.js +101 -0
- package/lib/types/package-identity.js +47 -0
- package/lib/types/params.js +328 -0
- package/lib/types/payload.js +36 -0
- package/lib/types/platform-line.generated.d.ts +10 -0
- package/lib/types/platform-line.generated.js +12 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/platform-services.js +52 -0
- package/lib/types/probe.js +122 -0
- package/lib/types/prompts.js +409 -0
- package/lib/types/quality.js +308 -0
- package/lib/types/reachability.js +122 -0
- package/lib/types/redact.js +257 -0
- package/lib/types/reference-rewrite.js +144 -0
- package/lib/types/reports.js +110 -0
- package/lib/types/restrictor.d.ts +11 -0
- package/lib/types/restrictor.js +48 -0
- package/lib/types/review-channel.js +59 -0
- package/lib/types/run-registry.js +345 -0
- package/lib/types/scope.js +7 -0
- package/lib/types/serial.js +17 -0
- package/lib/types/session-fold.d.ts +15 -0
- package/lib/types/session-fold.js +28 -0
- package/lib/types/session-projection.js +295 -0
- package/lib/types/signals.js +209 -0
- package/lib/types/skill-discovery.js +66 -0
- package/lib/types/skill-health.js +134 -0
- package/lib/types/skill-history.js +745 -0
- package/lib/types/skill-lint.js +112 -0
- package/lib/types/skill-reads.js +67 -0
- package/lib/types/skill-row-cells.js +123 -0
- package/lib/types/skill-store.js +3528 -0
- package/lib/types/state-store.js +60 -0
- package/lib/types/store-ledger.js +117 -0
- package/lib/types/subagent/channel.d.ts +4 -0
- package/lib/types/subagent/channel.js +131 -0
- package/lib/types/subagent/index.d.ts +4 -0
- package/lib/types/subagent/index.js +5 -0
- package/lib/types/subagent/readiness.d.ts +6 -0
- package/lib/types/subagent/readiness.js +35 -0
- package/lib/types/subagent/types.d.ts +58 -0
- package/lib/types/subagent/types.js +2 -0
- package/lib/types/threats.js +338 -0
- package/lib/types/tool-dispatch.d.ts +5 -0
- package/lib/types/tool-dispatch.js +535 -0
- package/lib/types/tree-write.js +40 -0
- package/lib/types/unit-members.js +121 -0
- package/lib/types/usage.js +474 -0
- package/lib/types/write-authority.js +23 -0
- package/lib/types/write-gates.js +344 -0
- package/lib/types/write-inventory.js +158 -0
- package/package.json +39 -11
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { n as errorText } from "./errors-Br2-uUSK.js";
|
|
2
|
+
//#region lib/types/restrictor.js
|
|
3
|
+
const name = "evolution-restrictor";
|
|
4
|
+
const inject = ["agentPresets"];
|
|
5
|
+
function normalize(names) {
|
|
6
|
+
return [...new Set((names ?? []).map((value) => value.trim()).filter((value) => value !== ""))].sort();
|
|
7
|
+
}
|
|
8
|
+
function failure(detail) {
|
|
9
|
+
return new Error(errorText("e-324-restricted-preset-unavailable", { a1: "evolution restrictor " + detail }));
|
|
10
|
+
}
|
|
11
|
+
function apply(ctx, config = {}) {
|
|
12
|
+
const allow = normalize(config.allow);
|
|
13
|
+
const deny = normalize(config.deny);
|
|
14
|
+
if (allow.length === 0 && deny.length === 0) throw failure("needs a non-empty allow or deny set");
|
|
15
|
+
const filter = {
|
|
16
|
+
...allow.length > 0 ? { allow } : {},
|
|
17
|
+
...deny.length > 0 ? { deny } : {}
|
|
18
|
+
};
|
|
19
|
+
const presetId = typeof config.presetId === "string" && config.presetId.trim() !== "" ? config.presetId : void 0;
|
|
20
|
+
if (presetId === void 0) throw failure("requires the restricted presetId in its row config");
|
|
21
|
+
const on = ctx.on;
|
|
22
|
+
if (typeof on !== "function") throw failure("requires a context that owns on()");
|
|
23
|
+
const presets = ctx.agentPresets;
|
|
24
|
+
if (presets === void 0 || typeof presets.composedPreset !== "function") throw failure("requires the agentPresets service (inject: agentPresets)");
|
|
25
|
+
on.call(ctx, "agent/created", (payload) => {
|
|
26
|
+
const agent = payload.agent;
|
|
27
|
+
if (agent === void 0) return;
|
|
28
|
+
if (presets.composedPreset(agent.ctx) !== presetId) return;
|
|
29
|
+
const tools = agent.ctx.tools;
|
|
30
|
+
if (tools === void 0 || typeof tools.restrict !== "function") throw failure("child scope exposes no tools.restrict()");
|
|
31
|
+
const effect = agent.ctx.effect;
|
|
32
|
+
if (effect === void 0) throw failure("child scope owns no effect() to release the restriction");
|
|
33
|
+
effect.call(agent.ctx, () => tools.restrict(filter), "evolution-restrictor:child");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { h as restrictedPresetId, n as ensureRestrictedPreset, r as restrictedPresetState } from "../ensure-MEYMj9jn.js";
|
|
2
|
+
//#region lib/types/subagent/readiness.js
|
|
3
|
+
function serviceOf$1(ctx, name) {
|
|
4
|
+
return ctx.get(name);
|
|
5
|
+
}
|
|
6
|
+
function kindOf(kind) {
|
|
7
|
+
return kind === "reviewer" ? "reviewer" : "maintainer";
|
|
8
|
+
}
|
|
9
|
+
/** Pure derivation (no side effects): the doctor reads this, the channel re-probes on every run. */
|
|
10
|
+
function probeChannelState(ctx, kind) {
|
|
11
|
+
const subagents = serviceOf$1(ctx, "subagents");
|
|
12
|
+
if (subagents === void 0 || typeof subagents.getProvider !== "function") return {
|
|
13
|
+
kind: "unavailable",
|
|
14
|
+
code: "E-323",
|
|
15
|
+
reason: "subagents service unavailable"
|
|
16
|
+
};
|
|
17
|
+
const provider = subagents.getProvider("spawn");
|
|
18
|
+
if (provider === void 0) return {
|
|
19
|
+
kind: "unavailable",
|
|
20
|
+
code: "E-323",
|
|
21
|
+
reason: "spawn provider unavailable"
|
|
22
|
+
};
|
|
23
|
+
if (provider.capabilities?.agentPreset !== true) return {
|
|
24
|
+
kind: "unavailable",
|
|
25
|
+
code: "E-323",
|
|
26
|
+
reason: "spawn provider lacks the agentPreset capability"
|
|
27
|
+
};
|
|
28
|
+
if (provider.inheritsParentContext === true) return {
|
|
29
|
+
kind: "unavailable",
|
|
30
|
+
code: "E-323",
|
|
31
|
+
reason: "spawn provider inherits the parent context (independent preset required)"
|
|
32
|
+
};
|
|
33
|
+
const agentPresets = serviceOf$1(ctx, "agentPresets");
|
|
34
|
+
if (agentPresets === void 0 || typeof agentPresets.readDocument !== "function" || typeof agentPresets.register !== "function") return {
|
|
35
|
+
kind: "unavailable",
|
|
36
|
+
code: "E-323",
|
|
37
|
+
reason: "agentPresets service unavailable or incomplete"
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
kind: "available",
|
|
41
|
+
provider: "spawn",
|
|
42
|
+
preset: {
|
|
43
|
+
id: restrictedPresetId(kindOf(kind)),
|
|
44
|
+
registered: restrictedPresetState(ctx, kindOf(kind))?.kind === "ready"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function subagentChannelState(ctx, kind) {
|
|
49
|
+
return probeChannelState(ctx, kind);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region lib/types/subagent/channel.js
|
|
53
|
+
function serviceOf(ctx, name) {
|
|
54
|
+
return ctx.get(name);
|
|
55
|
+
}
|
|
56
|
+
function messageOf(error) {
|
|
57
|
+
return error instanceof Error ? error.message : String(error);
|
|
58
|
+
}
|
|
59
|
+
function createSubagentChannel(ctx) {
|
|
60
|
+
let lastKind = "reviewer";
|
|
61
|
+
return {
|
|
62
|
+
get state() {
|
|
63
|
+
return probeChannelState(ctx, lastKind);
|
|
64
|
+
},
|
|
65
|
+
prepare(spec) {
|
|
66
|
+
lastKind = spec.kind;
|
|
67
|
+
const state = probeChannelState(ctx, spec.kind);
|
|
68
|
+
if (state.kind === "unavailable") return Promise.resolve(state);
|
|
69
|
+
if (spec.allow.length === 0) return Promise.resolve({
|
|
70
|
+
kind: "unavailable",
|
|
71
|
+
code: "E-324",
|
|
72
|
+
reason: "allow set is empty"
|
|
73
|
+
});
|
|
74
|
+
return Promise.resolve(state);
|
|
75
|
+
},
|
|
76
|
+
async run(spec) {
|
|
77
|
+
lastKind = spec.kind;
|
|
78
|
+
const probe = probeChannelState(ctx, spec.kind);
|
|
79
|
+
if (probe.kind === "unavailable") return probe;
|
|
80
|
+
const subagents = serviceOf(ctx, "subagents");
|
|
81
|
+
const presets = serviceOf(ctx, "agentPresets");
|
|
82
|
+
if (subagents === void 0 || presets === void 0) return {
|
|
83
|
+
kind: "unavailable",
|
|
84
|
+
code: "E-323",
|
|
85
|
+
reason: "subagents/agentPresets service unavailable"
|
|
86
|
+
};
|
|
87
|
+
const parentPresetId = presets.composedPreset(spec.parent.ctx);
|
|
88
|
+
if (parentPresetId === void 0) return {
|
|
89
|
+
kind: "unavailable",
|
|
90
|
+
code: "E-323",
|
|
91
|
+
reason: "parent preset could not be resolved"
|
|
92
|
+
};
|
|
93
|
+
const ensured = await ensureRestrictedPreset(ctx, {
|
|
94
|
+
kind: spec.kind,
|
|
95
|
+
parentPresetId,
|
|
96
|
+
allow: spec.allow
|
|
97
|
+
});
|
|
98
|
+
if (ensured.kind === "failed") return {
|
|
99
|
+
kind: "unavailable",
|
|
100
|
+
code: "E-324",
|
|
101
|
+
reason: ensured.reason
|
|
102
|
+
};
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
controller.abort(/* @__PURE__ */ new Error("subagent timeout"));
|
|
106
|
+
}, spec.timeoutMs);
|
|
107
|
+
const onAbort = () => {
|
|
108
|
+
controller.abort(spec.signal?.reason);
|
|
109
|
+
};
|
|
110
|
+
spec.signal?.addEventListener("abort", onAbort, { once: true });
|
|
111
|
+
let activation;
|
|
112
|
+
try {
|
|
113
|
+
activation = await subagents.startActivation({
|
|
114
|
+
provider: "spawn",
|
|
115
|
+
label: spec.label,
|
|
116
|
+
delivery: "caller",
|
|
117
|
+
request: {
|
|
118
|
+
parent: spec.parent,
|
|
119
|
+
prompt: [{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: spec.promptText
|
|
122
|
+
}],
|
|
123
|
+
agentPreset: ensured.presetId,
|
|
124
|
+
...spec.outputSchema !== void 0 ? { outputSchema: spec.outputSchema } : {},
|
|
125
|
+
...spec.maxDepth !== void 0 ? { maxDepth: spec.maxDepth } : {},
|
|
126
|
+
...spec.agentOptions !== void 0 ? { agentOptions: spec.agentOptions } : {}
|
|
127
|
+
},
|
|
128
|
+
signal: controller.signal
|
|
129
|
+
});
|
|
130
|
+
let result;
|
|
131
|
+
try {
|
|
132
|
+
result = await activation.result;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (controller.signal.aborted) return { kind: "aborted" };
|
|
135
|
+
return {
|
|
136
|
+
kind: "unavailable",
|
|
137
|
+
code: "E-323",
|
|
138
|
+
reason: messageOf(error)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let evidence;
|
|
142
|
+
if (spec.afterResult !== void 0) try {
|
|
143
|
+
const child = serviceOf(ctx, "agents")?.get(activation.childId);
|
|
144
|
+
if (child !== void 0) evidence = spec.afterResult(child);
|
|
145
|
+
} catch {
|
|
146
|
+
evidence = void 0;
|
|
147
|
+
}
|
|
148
|
+
const stopReason = result.stopReason;
|
|
149
|
+
if (stopReason === "completed" && result.structured !== void 0 && result.structured !== null) return {
|
|
150
|
+
kind: "ok",
|
|
151
|
+
structured: result.structured,
|
|
152
|
+
childId: activation.childId,
|
|
153
|
+
stopReason: "completed",
|
|
154
|
+
...evidence !== void 0 ? { evidence } : {}
|
|
155
|
+
};
|
|
156
|
+
if (stopReason === "aborted") return { kind: "aborted" };
|
|
157
|
+
return {
|
|
158
|
+
kind: "no-structured",
|
|
159
|
+
childId: activation.childId,
|
|
160
|
+
stopReason,
|
|
161
|
+
...result.diagnostic !== void 0 ? { diagnostic: result.diagnostic } : {},
|
|
162
|
+
...evidence !== void 0 ? { evidence } : {}
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (controller.signal.aborted) return { kind: "aborted" };
|
|
166
|
+
return {
|
|
167
|
+
kind: "unavailable",
|
|
168
|
+
code: "E-323",
|
|
169
|
+
reason: messageOf(error)
|
|
170
|
+
};
|
|
171
|
+
} finally {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
spec.signal?.removeEventListener("abort", onAbort);
|
|
174
|
+
if (activation !== void 0) try {
|
|
175
|
+
await activation.dispose();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
try {
|
|
178
|
+
ctx.logger.warn("subagent channel: activation dispose failed: " + messageOf(error));
|
|
179
|
+
} catch {}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
export { createSubagentChannel, probeChannelState, subagentChannelState };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Citation resolution for skill bodies and support files (design §5.2).
|
|
3
|
+
*
|
|
4
|
+
* WHY this exists beside `supportRefs` (skill-store.ts): that scanner answers
|
|
5
|
+
* "does this text contain a path-shaped token", which is the right question
|
|
6
|
+
* when a package is being ARCHIVED — every such path may dangle — and the wrong
|
|
7
|
+
* one when a section is only being MOVED, where nothing leaves and the only
|
|
8
|
+
* thing that changes is the directory a citation is read from. Measured false
|
|
9
|
+
* positives of the token scan: a URL tail (`https://host/references/x.md`), a
|
|
10
|
+
* category path the original Hermes documents as legitimate
|
|
11
|
+
* (`skills/scripts/foo.py`), and a prose command line
|
|
12
|
+
* (`npm run scripts/build.mjs`).
|
|
13
|
+
*
|
|
14
|
+
* CONTRACT: a token whose path STARTS WITH a support directory is a citation of
|
|
15
|
+
* this skill and is resolved from the SKILL ROOT, in whichever file it is
|
|
16
|
+
* written. A token that merely contains a support directory further along its
|
|
17
|
+
* path belongs to something else: it is reported as `foreign` and never
|
|
18
|
+
* resolved.
|
|
19
|
+
*
|
|
20
|
+
* PURE: existence is decided against the caller's file list, so identical
|
|
21
|
+
* inputs always answer the same report.
|
|
22
|
+
*/
|
|
23
|
+
/** Support directories that make a path a citation of the owning skill. */
|
|
24
|
+
export const CITATION_SUPPORT_DIRS = ['references', 'templates', 'scripts', 'assets'];
|
|
25
|
+
/** Scan budget: bounded so a pathological body cannot stall a maintenance run. */
|
|
26
|
+
export const DEFAULT_CITATION_REF_BUDGET = 500;
|
|
27
|
+
/** Words that introduce an executable command rather than a citation. */
|
|
28
|
+
const PROSE_COMMAND_PREFIXES = ['npm', 'pnpm', 'yarn', 'npx', 'node', 'python', 'python3', 'bash', 'sh', 'git', 'gh', 'run'];
|
|
29
|
+
const TOKEN_RE = /(?:references|templates|scripts|assets)\/[A-Za-z0-9._/-]+/g;
|
|
30
|
+
const TOKEN_CHAR_RE = /[A-Za-z0-9._/-]/;
|
|
31
|
+
const FENCE_RE = /^\s*(`{3,}|~{3,})/;
|
|
32
|
+
const TRAILING_PUNCT_RE = /[.,;:]+$/;
|
|
33
|
+
/** A citation's final segment must look like \u0060name.ext\u0060: a bare \u0060.\u0060 or a dotless
|
|
34
|
+
* segment is a directory mention, not a file. */
|
|
35
|
+
const FILE_SEGMENT_RE = /^[^.].*\.[^.]+$/;
|
|
36
|
+
const FRAGMENT_RE = /^#([A-Za-z0-9._-]+)/;
|
|
37
|
+
/** Whether the token starting at `start` is the argument of a shell command. */
|
|
38
|
+
function isProseCommand(line, start) {
|
|
39
|
+
const head = line.slice(0, start).trimEnd();
|
|
40
|
+
if (head === '')
|
|
41
|
+
return false;
|
|
42
|
+
const word = head.split(/\s+/).pop() ?? '';
|
|
43
|
+
return PROSE_COMMAND_PREFIXES.includes(word);
|
|
44
|
+
}
|
|
45
|
+
/** The token's final path segment (`references/a/b.md` -> `b.md`). */
|
|
46
|
+
function lastSegmentOf(target) {
|
|
47
|
+
return target.slice(target.lastIndexOf('/') + 1);
|
|
48
|
+
}
|
|
49
|
+
/** The token's parent directory within the skill (`references/a/b.md` -> `references/a`). */
|
|
50
|
+
function parentDirOf(target) {
|
|
51
|
+
const cut = target.lastIndexOf('/');
|
|
52
|
+
return cut <= 0 ? '' : target.slice(0, cut);
|
|
53
|
+
}
|
|
54
|
+
/** Classify one token by its first path segment. */
|
|
55
|
+
function classifyToken(token) {
|
|
56
|
+
const head = token.split('/')[0] ?? '';
|
|
57
|
+
return CITATION_SUPPORT_DIRS.includes(head) ? 'citation' : 'foreign';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve every path-shaped token in `content`.
|
|
61
|
+
* @param input - content, the skill-root-relative path of its owner, the skill's
|
|
62
|
+
* file list, and an optional scan budget.
|
|
63
|
+
* @returns the refs plus the dangling and foreign subsets; `truncated` marks a
|
|
64
|
+
* report that stopped at the budget.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveCitations(input) {
|
|
67
|
+
const budget = input.budget ?? DEFAULT_CITATION_REF_BUDGET;
|
|
68
|
+
const files = new Set(input.files.map(path => path.replace(/\\/g, '/')));
|
|
69
|
+
const refs = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
let truncated = false;
|
|
72
|
+
let fence = null;
|
|
73
|
+
const lines = input.content.split('\n');
|
|
74
|
+
for (let index = 0; index < lines.length && !truncated; index += 1) {
|
|
75
|
+
const rawLine = lines[index] ?? '';
|
|
76
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
77
|
+
const fenceMark = FENCE_RE.exec(line);
|
|
78
|
+
if (fenceMark) {
|
|
79
|
+
const marker = fenceMark[1] ?? '';
|
|
80
|
+
fence = fence === null ? marker.slice(0, 1) : null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const match of line.matchAll(TOKEN_RE)) {
|
|
84
|
+
const matched = match[0];
|
|
85
|
+
const matchedAt = match.index;
|
|
86
|
+
let start = matchedAt;
|
|
87
|
+
while (start > 0 && TOKEN_CHAR_RE.test(line[start - 1] ?? ''))
|
|
88
|
+
start -= 1;
|
|
89
|
+
const prefix = line.slice(0, start);
|
|
90
|
+
const tail = line.slice(matchedAt + matched.length);
|
|
91
|
+
const fragment = FRAGMENT_RE.exec(tail)?.[1];
|
|
92
|
+
const token = matched.replace(TRAILING_PUNCT_RE, '');
|
|
93
|
+
const scanned = line.slice(start, matchedAt + matched.length);
|
|
94
|
+
const classified = fence !== null
|
|
95
|
+
? 'fence'
|
|
96
|
+
: prefix.endsWith(':') && line.slice(start, start + 2) === '//'
|
|
97
|
+
? 'url'
|
|
98
|
+
: isProseCommand(line, start)
|
|
99
|
+
? 'prose'
|
|
100
|
+
: classifyToken(scanned);
|
|
101
|
+
// A citation names a FILE: a dotless last segment is a directory mention
|
|
102
|
+
// (`references/templates/scripts` in prose), and a `..` segment is a
|
|
103
|
+
// traversal — a different defect class the old scanner also skipped. Both
|
|
104
|
+
// are prose unless the listing proves the path is a real entry.
|
|
105
|
+
const traversal = scanned.split('/').includes('..');
|
|
106
|
+
const namesFile = !traversal && (FILE_SEGMENT_RE.test(lastSegmentOf(scanned)) || files.has(token));
|
|
107
|
+
const kind = classified === 'citation' && !namesFile ? 'prose' : classified;
|
|
108
|
+
const target = kind === 'citation' ? token : null;
|
|
109
|
+
const key = `${index}|${kind}|${target ?? token}`;
|
|
110
|
+
if (seen.has(key))
|
|
111
|
+
continue;
|
|
112
|
+
seen.add(key);
|
|
113
|
+
refs.push({
|
|
114
|
+
raw: token,
|
|
115
|
+
line: index + 1,
|
|
116
|
+
target,
|
|
117
|
+
base: 'root',
|
|
118
|
+
kind,
|
|
119
|
+
exists: target !== null && files.has(target),
|
|
120
|
+
...(fragment === undefined ? {} : { fragment }),
|
|
121
|
+
});
|
|
122
|
+
if (refs.length >= budget) {
|
|
123
|
+
truncated = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const citations = refs.filter(ref => ref.kind === 'citation');
|
|
129
|
+
const undecidable = (ref) => {
|
|
130
|
+
const target = ref.target;
|
|
131
|
+
return target !== null && !ref.exists && files.has(parentDirOf(target));
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
refs,
|
|
135
|
+
dangling: citations.filter(ref => !ref.exists && !undecidable(ref)),
|
|
136
|
+
unverified: citations.filter(undecidable),
|
|
137
|
+
foreign: refs.filter(ref => ref.kind !== 'citation'),
|
|
138
|
+
truncated,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** The sanctioned body hook (design §5.6): a list item whose payload is
|
|
142
|
+
* exactly one support-file path, so the file stays discoverable in the body. */
|
|
143
|
+
export const HOOK_LINE_RE = /^- [^\n]{1,60} → ((?:references|templates|scripts|assets)\/[a-z0-9][a-z0-9._/-]*)$/;
|
|
144
|
+
/** The retirement escape hatch (design §5.6): a standalone body comment that
|
|
145
|
+
* names the support file it exempts and the reason it must survive. The path
|
|
146
|
+
* is part of the marker because the proposal list is per FILE, and a reason is
|
|
147
|
+
* required — an unexplained exemption is what this hook exists to prevent. */
|
|
148
|
+
export const KEEP_LINE_RE = /^<!--\s*keep:\s*((?:references|templates|scripts|assets)\/[a-z0-9][a-z0-9._/-]*)\s+(\S[^>]*?)\s*-->$/;
|
|
149
|
+
/** Whether a skill-relative path names a FILE rather than a directory: the same
|
|
150
|
+
* rule the scanner applies to a token's final segment (`name.ext`). The support
|
|
151
|
+
* listing is one level deep and includes directory entries (a real library has
|
|
152
|
+
* `references/archive`), which no pointer or retirement list may treat as a file.
|
|
153
|
+
* @param path - a skill-root-relative support path.
|
|
154
|
+
* @returns true when the final segment is file-shaped.
|
|
155
|
+
*/
|
|
156
|
+
export function isFileShapedPath(path) {
|
|
157
|
+
return FILE_SEGMENT_RE.test(path.slice(path.lastIndexOf('/') + 1));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Scan a body for sanctioned hooks and `keep` markers.
|
|
161
|
+
* @param content - the SKILL.md body (frontmatter included).
|
|
162
|
+
* @returns hook targets plus the `keep` reasons keyed by target.
|
|
163
|
+
*/
|
|
164
|
+
export function scanBodyHooks(content) {
|
|
165
|
+
const targets = [];
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
const kept = new Map();
|
|
168
|
+
// A hook-shaped line inside a fenced block is sample text, not a pointer.
|
|
169
|
+
let fence = null;
|
|
170
|
+
for (const rawLine of content.split('\n')) {
|
|
171
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
172
|
+
const fenceMark = FENCE_RE.exec(line);
|
|
173
|
+
if (fenceMark) {
|
|
174
|
+
fence = fence === null ? (fenceMark[1] ?? '').slice(0, 1) : null;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (fence !== null)
|
|
178
|
+
continue;
|
|
179
|
+
const keep = KEEP_LINE_RE.exec(line);
|
|
180
|
+
if (keep?.[1] !== undefined) {
|
|
181
|
+
kept.set(keep[1], (keep[2] ?? '').trim());
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const target = HOOK_LINE_RE.exec(line)?.[1];
|
|
185
|
+
if (target === undefined || seen.has(target))
|
|
186
|
+
continue;
|
|
187
|
+
seen.add(target);
|
|
188
|
+
targets.push(target);
|
|
189
|
+
}
|
|
190
|
+
return { targets, kept };
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=citations.js.map
|
|
@@ -304,4 +304,4 @@ export declare function trimTrailingBlankLines(lines: string[]): string[];
|
|
|
304
304
|
*/
|
|
305
305
|
export declare function presetPatchText(patchText: string): string;
|
|
306
306
|
export {};
|
|
307
|
-
//# sourceMappingURL=
|
|
307
|
+
//# sourceMappingURL=composer.d.ts.map
|