@lmzhen/dsh-evolution-core 0.26.26 → 0.27.0
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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library-level drift signals for the maintenance subagent (design 011).
|
|
3
|
+
*
|
|
4
|
+
* Deterministic fact checks over a skill-library snapshot: domain drift
|
|
5
|
+
* (narrow names, near-duplicate groups, prefix clusters) and layer drift
|
|
6
|
+
* (log-like bodies, duplicate headings, overlong lines, missing support-file
|
|
7
|
+
* pointers, description over the authoring bar). Pure functions only — no IO,
|
|
8
|
+
* no LLM, no services. Thresholds are imported from their owning modules
|
|
9
|
+
* (skill-health / quality / skill-store), never duplicated.
|
|
10
|
+
*
|
|
11
|
+
* Distinct from `signals.ts` — the session-level review signal gate.
|
|
12
|
+
*/
|
|
13
|
+
import { assessStructureHealth, DEFAULT_HEALTH_THRESHOLDS } from "./skill-health.js";
|
|
14
|
+
import { computeDedupGroups, computePrefixClusters, LOW_QUALITY_THRESHOLD } from "./quality.js";
|
|
15
|
+
import { AUTHORING_DESCRIPTION_BAR, DEFAULT_STALE_AFTER_DAYS, MAX_SKILL_CONTENT_CHARS } from "./constants.js";
|
|
16
|
+
import { isFileShapedPath, scanBodyHooks } from "./citations.js";
|
|
17
|
+
import { tokenLineFor } from "./cost.js";
|
|
18
|
+
/** Physical line length at/above which a body line is reported overlong (011 §4). */
|
|
19
|
+
export const DRIFT_MAX_LINE_CHARS = 1_500;
|
|
20
|
+
/** Signal-set version: bump whenever ids/thresholds change (011 §7 version coupling). */
|
|
21
|
+
export const DRIFT_SIGNALS_VERSION = '3';
|
|
22
|
+
/** Render-time nouns for the MAINTAIN_PROMPT placeholders (single vocabulary with the facts block). */
|
|
23
|
+
export const DRIFT_SIGNAL_NOUNS = {
|
|
24
|
+
dedup_group: '近重复组',
|
|
25
|
+
prefix_cluster: '前缀聚类',
|
|
26
|
+
stamp_density: 'stamp 密度',
|
|
27
|
+
body_size: '正文体量',
|
|
28
|
+
dup_heading: '重复标题',
|
|
29
|
+
overlong_line: '超长行',
|
|
30
|
+
pointer_missing: '缺失指针',
|
|
31
|
+
citation_resolution: '引用解析',
|
|
32
|
+
demand: '需求证据',
|
|
33
|
+
description_chars: '描述长度',
|
|
34
|
+
narrow_name: '窄名',
|
|
35
|
+
usage_observed: '使用观察',
|
|
36
|
+
quality_low: '质量分',
|
|
37
|
+
};
|
|
38
|
+
const NARROW_NAME_PATTERNS = [
|
|
39
|
+
{ label: 'error-string', re: /^(?:err|error|exception|traceback|warn|fail)(?:[-_][a-z0-9]+)+$/i },
|
|
40
|
+
{ label: 'pr-number', re: /^(?:pr|issue)[-_]?\d{2,}$/i },
|
|
41
|
+
{ label: 'dated', re: /\d{4}-\d{2}-\d{2}/ },
|
|
42
|
+
{ label: 'session-verb', re: /^(?:fix|debug|audit|salvage|diagnose|investigate)[-_][a-z0-9-]+$/i },
|
|
43
|
+
];
|
|
44
|
+
/** Detect support files the body never references (by basename or relative path). */
|
|
45
|
+
export function missingSupportPointers(body, supportFiles) {
|
|
46
|
+
return supportFiles.filter((path) => {
|
|
47
|
+
const base = path.split('/').pop() ?? path;
|
|
48
|
+
return base.length > 0 && !body.includes(base) && !body.includes(path);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Support files the body mentions WITHOUT the sanctioned hook form (design
|
|
52
|
+
* §5.6). Such a mention still counts as a pointer for `pointer_missing`, but
|
|
53
|
+
* only the hook form keeps the file discoverable after a move — detail only.
|
|
54
|
+
* A file carrying a `keep` marker is exempt: its mention IS the marker. */
|
|
55
|
+
export function unhookedSupportPointers(body, supportFiles) {
|
|
56
|
+
const { targets, kept } = scanBodyHooks(body);
|
|
57
|
+
const hooked = new Set(targets);
|
|
58
|
+
return supportFiles.filter((path) => {
|
|
59
|
+
if (!isFileShapedPath(path))
|
|
60
|
+
return false;
|
|
61
|
+
const base = path.split('/').pop() ?? path;
|
|
62
|
+
if (kept.has(path) || (base.length > 0 && kept.has(base)))
|
|
63
|
+
return false;
|
|
64
|
+
const mentioned = (base.length > 0 && body.includes(base)) || body.includes(path);
|
|
65
|
+
return mentioned && !hooked.has(path) && !hooked.has(base);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Support files with no readers, no citations and no `keep` marker whose owning
|
|
69
|
+
* skill has been idle for at least the lifecycle stale window. PROPOSAL INPUT
|
|
70
|
+
* only — nothing retires a file on its own — and an unmeasurable input (no age
|
|
71
|
+
* evidence, no citation scan) yields an empty list WITH its reason, never a
|
|
72
|
+
* silent "nothing qualifies".
|
|
73
|
+
* @param snapshot - the skill's drift snapshot.
|
|
74
|
+
* @param supportFiles - its enumerated support files, in listing order.
|
|
75
|
+
* @returns the candidates plus the status that explains an empty list.
|
|
76
|
+
*/
|
|
77
|
+
export function retirementReport(snapshot, supportFiles) {
|
|
78
|
+
const idle = snapshot.liveness?.idleDays;
|
|
79
|
+
if (idle === undefined)
|
|
80
|
+
return { candidates: [], status: 'no-age' };
|
|
81
|
+
if (snapshot.citations === undefined)
|
|
82
|
+
return { candidates: [], status: 'unscanned' };
|
|
83
|
+
if (idle < DEFAULT_STALE_AFTER_DAYS)
|
|
84
|
+
return { candidates: [], status: 'none' };
|
|
85
|
+
const cited = new Set();
|
|
86
|
+
for (const ref of snapshot.citations.refs) {
|
|
87
|
+
if (ref.kind === 'citation' && ref.target !== null)
|
|
88
|
+
cited.add(ref.target);
|
|
89
|
+
}
|
|
90
|
+
const { kept } = scanBodyHooks(snapshot.body);
|
|
91
|
+
const demand = snapshot.demand ?? {};
|
|
92
|
+
const candidates = supportFiles
|
|
93
|
+
.filter(path => isFileShapedPath(path) && (demand[path] ?? 0) === 0 && !cited.has(path) && !kept.has(path))
|
|
94
|
+
.map(path => ({ path, idleDays: Math.floor(idle) }));
|
|
95
|
+
return { candidates, status: candidates.length === 0 ? 'none' : 'listed' };
|
|
96
|
+
}
|
|
97
|
+
/** Duplicate `## heading` occurrences: singleton results default to head of the file. */
|
|
98
|
+
export function duplicateHeadings(body) {
|
|
99
|
+
const counts = new Map();
|
|
100
|
+
// S1.8 (v37 P1-14): scan the CR-stripped view — `.` never matches `\r` and a
|
|
101
|
+
// non-multiline `$` only anchors at the end of the input, so a CRLF body made
|
|
102
|
+
// every heading line unmatchable and `dup_heading` reported a FALSE `pass`
|
|
103
|
+
// (the maintenance prompt then never asked for the merge).
|
|
104
|
+
for (const raw of body.split('\n')) {
|
|
105
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
106
|
+
const m = /^##\s+(.+)$/.exec(line);
|
|
107
|
+
if (m?.[1]) {
|
|
108
|
+
const heading = m[1].trim();
|
|
109
|
+
if (heading)
|
|
110
|
+
counts.set(heading, (counts.get(heading) ?? 0) + 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return [...counts.entries()]
|
|
114
|
+
.filter(([, count]) => count > 1)
|
|
115
|
+
.map(([heading, count]) => ({ heading, count }));
|
|
116
|
+
}
|
|
117
|
+
/** Physical lines over `max` characters: `{ lineNo, chars }`, 1-based line numbers. */
|
|
118
|
+
export function overlongLines(body, max = DRIFT_MAX_LINE_CHARS) {
|
|
119
|
+
const out = [];
|
|
120
|
+
const lines = body.split('\n');
|
|
121
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
122
|
+
// S1.8 (v37 P2-3): the CR is a line terminator, not a visible character — a
|
|
123
|
+
// 1500-character CRLF line used to report 1501 and send the model after a
|
|
124
|
+
// line that already complies.
|
|
125
|
+
const raw = lines[index] ?? '';
|
|
126
|
+
const length = raw.endsWith('\r') ? raw.length - 1 : raw.length;
|
|
127
|
+
if (length > max)
|
|
128
|
+
out.push({ lineNo: index + 1, chars: length });
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** Narrow-name shapes detected in a skill name (empty = none). */
|
|
133
|
+
export function narrowNameMatches(name) {
|
|
134
|
+
return NARROW_NAME_PATTERNS.filter(({ re }) => re.test(name)).map(({ label }) => label);
|
|
135
|
+
}
|
|
136
|
+
function supportGroupCount(supportFiles) {
|
|
137
|
+
const groups = new Set();
|
|
138
|
+
for (const path of supportFiles ?? []) {
|
|
139
|
+
const head = path.split('/')[0];
|
|
140
|
+
if (head)
|
|
141
|
+
groups.add(head);
|
|
142
|
+
}
|
|
143
|
+
return groups.size;
|
|
144
|
+
}
|
|
145
|
+
function sig(id, verdict, value, threshold, detail) {
|
|
146
|
+
return { id, verdict, value, threshold, detail };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Compute all drift signals for a snapshot. Missing inputs (quality score,
|
|
150
|
+
* usage window) yield `unknown` — never a fabricated verdict.
|
|
151
|
+
*/
|
|
152
|
+
export function computeDriftSignals(snapshots) {
|
|
153
|
+
const library = [];
|
|
154
|
+
const names = snapshots.map(s => s.name);
|
|
155
|
+
const dedup = computeDedupGroups({
|
|
156
|
+
contents: new Map(snapshots.map(s => [s.name, s.body])),
|
|
157
|
+
});
|
|
158
|
+
// PLAN-R2 P2-8 (2026-09-16): the scan is budget-bounded; a truncated sweep
|
|
159
|
+
// must say so instead of reading as a clean pass.
|
|
160
|
+
const dedupTruncation = dedup.truncated ? ' (dedup scan truncated at the pair-comparison budget)' : '';
|
|
161
|
+
library.push(dedup.groups.length === 0
|
|
162
|
+
? sig('dedup_group', 'pass', `none${dedupTruncation}`, 'size >= 2')
|
|
163
|
+
: sig('dedup_group', 'over', `${dedup.groups.map(group => group.join(', ')).join(' | ')}${dedupTruncation}`, 'size >= 2', `members=${dedup.groups.map(group => group.join('|')).join(';')}`));
|
|
164
|
+
const clusters = computePrefixClusters(names);
|
|
165
|
+
library.push(clusters.length === 0
|
|
166
|
+
? sig('prefix_cluster', 'pass', 'none', 'size >= 2')
|
|
167
|
+
: sig('prefix_cluster', 'over', clusters.map(cluster => cluster.members.join(', ')).join(' | '), 'size >= 2', `key=${clusters.map(cluster => cluster.key).join('|')}`));
|
|
168
|
+
const allProvided = snapshots.length > 0 && snapshots.every(s => s.usageObserved !== null && s.usageObserved !== undefined);
|
|
169
|
+
library.push(!allProvided
|
|
170
|
+
? sig('usage_observed', 'unknown', 'not-observed', undefined, 'usage window status missing')
|
|
171
|
+
: snapshots.every(s => s.usageObserved === true)
|
|
172
|
+
? sig('usage_observed', 'pass', 'observed')
|
|
173
|
+
: sig('usage_observed', 'pass', 'unobserved'));
|
|
174
|
+
const skills = snapshots.map((snapshot) => {
|
|
175
|
+
const signals = [];
|
|
176
|
+
const body = snapshot.body;
|
|
177
|
+
const supportFiles = snapshot.supportFiles ?? [];
|
|
178
|
+
const supportEnumerated = snapshot.supportFiles !== undefined;
|
|
179
|
+
const health = assessStructureHealth({
|
|
180
|
+
skillName: snapshot.name,
|
|
181
|
+
bodyChars: body.length,
|
|
182
|
+
bodyText: body,
|
|
183
|
+
supportGroups: supportGroupCount(supportFiles),
|
|
184
|
+
}, DEFAULT_HEALTH_THRESHOLDS);
|
|
185
|
+
const density = health.dims.stampDensityPerKb;
|
|
186
|
+
// P2-12 (v39): density stays null only for an empty body or one below
|
|
187
|
+
// MIN_STAMP_BODY_CHARS (skill-health.ts gates the measurement there), so
|
|
188
|
+
// 'below-min-body' is the only reachable reason — 'not-assessed' was dead.
|
|
189
|
+
signals.push(density === null
|
|
190
|
+
? sig('stamp_density', 'pass', 'below-min-body')
|
|
191
|
+
: sig('stamp_density', density >= DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb ? 'over' : 'pass', `${density.toFixed(2)}/KB`, `${DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb}/KB`));
|
|
192
|
+
// V6: judged on the TOKEN scale. The threshold is the borrowed upstream
|
|
193
|
+
// character line, converted per body composition (tokenLineFor), so the textual
|
|
194
|
+
// trigger stays "20k characters" while value/threshold/detail all speak tokens.
|
|
195
|
+
// Without a cost breakdown the character line decides (never a silent pass).
|
|
196
|
+
const cost = snapshot.cost;
|
|
197
|
+
const lineTokens = cost === undefined ? null : tokenLineFor(DEFAULT_HEALTH_THRESHOLDS.softBodyChars, cost);
|
|
198
|
+
const overLine = cost === undefined || lineTokens === null
|
|
199
|
+
? body.length >= DEFAULT_HEALTH_THRESHOLDS.softBodyChars
|
|
200
|
+
: cost.tokens >= lineTokens;
|
|
201
|
+
signals.push(sig('body_size', overLine ? 'over' : 'pass', cost === undefined ? `${body.length}` : `${cost.tokens} tokens / ${body.length} chars`, cost === undefined || lineTokens === null
|
|
202
|
+
? `${DEFAULT_HEALTH_THRESHOLDS.softBodyChars}`
|
|
203
|
+
: `line=${lineTokens} tokens (${DEFAULT_HEALTH_THRESHOLDS.softBodyChars} chars)`, cost === undefined
|
|
204
|
+
? undefined
|
|
205
|
+
// V3: the band is the AUTHORING discipline band (upstream's 20k split
|
|
206
|
+
// line, drawn on this body's token scale), and the multiple is what makes
|
|
207
|
+
// it actionable — a bare ceiling says nothing about how far past it is.
|
|
208
|
+
: `estimate range ${cost.tokensLow}-${cost.tokensHigh} tokens (4 chars/token basis); cjk=${cost.cjk}${lineTokens === null ? '' : `; line=${lineTokens}, body=${(cost.tokens / lineTokens).toFixed(1)}x`}${oversizeSupportNote(snapshot.supportChars)}`));
|
|
209
|
+
const dupes = duplicateHeadings(body);
|
|
210
|
+
signals.push(dupes.length === 0
|
|
211
|
+
? sig('dup_heading', 'pass', 'none')
|
|
212
|
+
: sig('dup_heading', 'over', dupes.map(d => `${d.heading}(${d.count})`).join(', '), 'count >= 2'));
|
|
213
|
+
const long = overlongLines(body);
|
|
214
|
+
signals.push(long.length === 0
|
|
215
|
+
? sig('overlong_line', 'pass', 'none')
|
|
216
|
+
: sig('overlong_line', 'over', long.map(l => `${l.lineNo}:${l.chars}`).join(', '), `${DRIFT_MAX_LINE_CHARS}`));
|
|
217
|
+
const missing = supportEnumerated ? missingSupportPointers(body, supportFiles) : undefined;
|
|
218
|
+
// Hook coverage rides the EXISTING signal as detail (design §5.6): a file
|
|
219
|
+
// mentioned without the sanctioned hook form is still pointed at, so the
|
|
220
|
+
// verdict stays with `missingSupportPointers`.
|
|
221
|
+
const unhooked = supportEnumerated ? unhookedSupportPointers(body, supportFiles) : [];
|
|
222
|
+
const unhookedDetail = unhooked.length === 0
|
|
223
|
+
? undefined
|
|
224
|
+
: `unhooked=${unhooked.length}: ${unhooked.slice(0, 5).join(', ')}`;
|
|
225
|
+
signals.push(!supportEnumerated
|
|
226
|
+
? sig('pointer_missing', 'unknown', 'not-enumerated', undefined, 'support files not enumerated')
|
|
227
|
+
: (missing ?? []).length === 0
|
|
228
|
+
? sig('pointer_missing', 'pass', 'none', undefined, unhookedDetail)
|
|
229
|
+
: sig('pointer_missing', 'over', missing?.join(', ') ?? '', undefined, unhookedDetail));
|
|
230
|
+
signals.push(citationSignal(snapshot.citations));
|
|
231
|
+
signals.push(demandSignal(snapshot, supportFiles, supportEnumerated));
|
|
232
|
+
const narrow = narrowNameMatches(snapshot.name);
|
|
233
|
+
signals.push(narrow.length === 0
|
|
234
|
+
? sig('narrow_name', 'pass', 'none')
|
|
235
|
+
: sig('narrow_name', 'over', narrow.join(', '), undefined, `name=${snapshot.name}`));
|
|
236
|
+
const description = snapshot.description;
|
|
237
|
+
signals.push(description === undefined
|
|
238
|
+
// C-21 (v10 audit): the threshold renders from the constant like the
|
|
239
|
+
// assessed branch below — the hardcoded '60' could drift from
|
|
240
|
+
// AUTHORING_DESCRIPTION_BAR.
|
|
241
|
+
? sig('description_chars', 'unknown', 'missing', `${AUTHORING_DESCRIPTION_BAR}`)
|
|
242
|
+
: sig('description_chars', description.length > AUTHORING_DESCRIPTION_BAR ? 'over' : 'pass', `${description.length}`, `${AUTHORING_DESCRIPTION_BAR}`));
|
|
243
|
+
const quality = snapshot.quality;
|
|
244
|
+
signals.push(quality === null || quality === undefined
|
|
245
|
+
? sig('quality_low', 'unknown', 'not-assessed')
|
|
246
|
+
: sig('quality_low', quality < LOW_QUALITY_THRESHOLD ? 'over' : 'pass', quality.toFixed(2), `${LOW_QUALITY_THRESHOLD}`));
|
|
247
|
+
return {
|
|
248
|
+
name: snapshot.name,
|
|
249
|
+
signals,
|
|
250
|
+
...(snapshot.protected !== undefined && snapshot.protected !== null ? { protected: snapshot.protected } : {}),
|
|
251
|
+
...(snapshot.catalogInvalid !== undefined ? { catalogInvalid: snapshot.catalogInvalid } : {}),
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
return { library, skills };
|
|
255
|
+
}
|
|
256
|
+
/** Dangling-citation verdict: a partial scan is `unknown`, never a clean pass. */
|
|
257
|
+
function citationSignal(citations) {
|
|
258
|
+
if (citations === undefined)
|
|
259
|
+
return sig('citation_resolution', 'unknown', 'not-scanned', undefined, 'support-file list missing');
|
|
260
|
+
if (citations.truncated)
|
|
261
|
+
return sig('citation_resolution', 'unknown', `truncated at ${citations.refs.length} refs`, undefined, 'partial scan — not a clean verdict');
|
|
262
|
+
const cited = citations.refs.filter(ref => ref.kind === 'citation');
|
|
263
|
+
if (citations.dangling.length > 0) {
|
|
264
|
+
const missing = citations.dangling.slice(0, 5).map(ref => ref.target ?? ref.raw).join(', ');
|
|
265
|
+
return sig('citation_resolution', 'over', `dangling=${citations.dangling.length}/${cited.length}`, 'dangling=0', `missing: ${missing}`);
|
|
266
|
+
}
|
|
267
|
+
return sig('citation_resolution', 'pass', `citations=${cited.length} foreign=${citations.foreign.length}${citations.unverified.length === 0 ? '' : ` unverified=${citations.unverified.length}`}`);
|
|
268
|
+
}
|
|
269
|
+
/** V4 (design §16.6): the report half of the support-file cap. Only files the
|
|
270
|
+
* assembler could PROVE large enough to be over the cap are namespaced here; an
|
|
271
|
+
* absent map claims nothing (unknown is not zero).
|
|
272
|
+
* @param supportChars - measured character counts, or undefined when unmeasured.
|
|
273
|
+
* @returns a detail suffix naming the oversize files, or an empty string.
|
|
274
|
+
*/
|
|
275
|
+
function oversizeSupportNote(supportChars) {
|
|
276
|
+
if (supportChars === undefined)
|
|
277
|
+
return '';
|
|
278
|
+
const oversize = Object.entries(supportChars)
|
|
279
|
+
.filter(([, chars]) => chars > MAX_SKILL_CONTENT_CHARS)
|
|
280
|
+
.sort((a, b) => b[1] - a[1]);
|
|
281
|
+
if (oversize.length === 0)
|
|
282
|
+
return '';
|
|
283
|
+
const shown = oversize.slice(0, 3).map(([path, chars]) => `${path}(${chars})`);
|
|
284
|
+
return `; oversize support file(s): ${shown.join(', ')}${oversize.length > shown.length ? ' …' : ''}`;
|
|
285
|
+
}
|
|
286
|
+
/** Render-time reason for an empty retirement list (design §5.6). */
|
|
287
|
+
const RETIREMENT_STATUS_TEXT = {
|
|
288
|
+
listed: 'listed',
|
|
289
|
+
none: 'none',
|
|
290
|
+
'no-age': 'age unknown',
|
|
291
|
+
unscanned: 'citations unscanned',
|
|
292
|
+
};
|
|
293
|
+
/** Cold-support-file verdict (design §5.5): zero reads are evidence only while
|
|
294
|
+
* the observation window is open and the file list is known. */
|
|
295
|
+
function demandSignal(snapshot, supportFiles, supportEnumerated) {
|
|
296
|
+
if (!supportEnumerated || supportFiles.length === 0) {
|
|
297
|
+
return sig('demand', 'unknown', 'not-enumerated', undefined, 'support files not enumerated');
|
|
298
|
+
}
|
|
299
|
+
if (snapshot.usageObserved !== true) {
|
|
300
|
+
return sig('demand', 'unknown', 'window-closed', undefined, 'no observed reads yet: zero is not evidence');
|
|
301
|
+
}
|
|
302
|
+
const demand = snapshot.demand ?? {};
|
|
303
|
+
const cold = supportFiles.filter(path => (demand[path] ?? 0) === 0);
|
|
304
|
+
// Retirement evidence rides the SAME signal as detail: it narrows "cold" to
|
|
305
|
+
// "cold AND uncited AND old enough AND not kept" without moving the verdict.
|
|
306
|
+
const retire = retirementReport(snapshot, supportFiles);
|
|
307
|
+
const retireDetail = retire.candidates.length === 0
|
|
308
|
+
? `retire: ${RETIREMENT_STATUS_TEXT[retire.status]}`
|
|
309
|
+
: `retire≥${DEFAULT_STALE_AFTER_DAYS}d: ${retire.candidates.slice(0, 5).map(c => `${c.path}(${c.idleDays}d)`).join(', ')}`;
|
|
310
|
+
if (cold.length === 0)
|
|
311
|
+
return sig('demand', 'pass', `${supportFiles.length} warm`, 'cold=0', retireDetail);
|
|
312
|
+
const sample = cold.slice(0, 5).join(', ');
|
|
313
|
+
return sig('demand', 'over', `cold=${cold.length}/${supportFiles.length}`, 'cold=0', `never read: ${sample} · ${retireDetail}`);
|
|
314
|
+
}
|
|
315
|
+
/** Convenience: fetch one signal from an assessment or library list. */
|
|
316
|
+
export function findDriftSignal(signals, id) {
|
|
317
|
+
return signals.find(signal => signal.id === id);
|
|
318
|
+
}
|
|
319
|
+
//# sourceMappingURL=drift-signals.js.map
|
package/lib/types/env.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evolution environment-variable reading, single-source (WC, 0.3.56).
|
|
3
|
+
*
|
|
4
|
+
* Every `DSH_EVOLUTION_*` value the PLUGIN code reads goes through this module
|
|
5
|
+
* so the trim/whitelist patterns (v10 P2-12/13) live in exactly one place and
|
|
6
|
+
* the generated env reference can point at one behavior. Configuration-layer
|
|
7
|
+
* reads inside patch YAML `!!js` expressions (session-query path/openAt) stay
|
|
8
|
+
* in the profile config evaluation — they are NOT migrated (they are resolved
|
|
9
|
+
* at cordis config time, not plugin time) but are documented in the README
|
|
10
|
+
* env table. `EVOLUTION_SCOPE` is read by the source installers only
|
|
11
|
+
* (`packages/scripts/install-layered.mjs`, `packages/test-support/row-contract.ts`),
|
|
12
|
+
* never by plugin runtime code.
|
|
13
|
+
*
|
|
14
|
+
* v14 P3-1: the former `EVOLUTION_ENV_KEYS` export was deleted — nothing read
|
|
15
|
+
* it, so the "generated env reference" it claimed to source was never
|
|
16
|
+
* generated (the README table is maintained by hand and now lists every key).
|
|
17
|
+
*/
|
|
18
|
+
const ALLOW_ROW_COLLISIONS = '1';
|
|
19
|
+
/** N-5 escape: `DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1` downgrades a delta-row
|
|
20
|
+
* collision from fail-loud to warn+keep-both. Any other value (including an
|
|
21
|
+
* EMPTY/whitespace string — a set-but-unset variable) keeps the fail-loud. */
|
|
22
|
+
export function allowRowCollisions(env = process.env) {
|
|
23
|
+
return env.DSH_EVOLUTION_ALLOW_ROW_COLLISIONS === ALLOW_ROW_COLLISIONS;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=env.js.map
|
package/lib/types/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE error-code table: E-301…E-
|
|
2
|
+
* THE error-code table: E-301…E-325 with the exact text each one emits (E-317…E-319 are the
|
|
3
3
|
* write-admission gates' own refusals).
|
|
4
4
|
*
|
|
5
5
|
* The codes used to be spelled out at 33 call sites across two packages, so one
|
|
@@ -39,6 +39,9 @@ export declare const EVOLUTION_ERRORS: Readonly<{
|
|
|
39
39
|
readonly 'e-322-a-parameter-would-break-a': "E-322: %a1% 至少应为 %a2%,但这次写入把它设成 %a3%。未写入任何内容。";
|
|
40
40
|
readonly 'e-321-no-io-provider-is': "E-321: 没有挂载 evolution-io 提供方,因此无法读取旧设置文档。请挂载 evolution-io 行(evolution-host/evolution-all)后重试。";
|
|
41
41
|
readonly 'e-319-skill-write-confirm-timed-out': "E-319: 技能 \"%a1%\" 未被%a2% —— 确认提示在 %a3% 秒内无人应答,因此该写入被取消。没有人在场决定:把 `skillWriteConfirm` 设为 `auto` 可免提示直接写入,保留 `ask` 则一直等到有人应答,或提高 `skillWriteConfirmTimeoutSeconds`。";
|
|
42
|
+
readonly 'e-323-subagent-channel-unavailable': "E-323: 子代理通道不可用:缺少 subagents/agentPresets 服务,或 spawn provider 不支持 agentPreset 能力。审阅/维护会明确标记为不可用,不会静默退回 inject。运行 /evolution doctor 查看原因与修复指引。";
|
|
43
|
+
readonly 'e-324-restricted-preset-unavailable': "E-324: 受限 preset 创建失败(读取父 preset、解析、注册或限制器挂载任一步失败):%a1%。审阅/维护通道不可用;运行 /evolution doctor 查看 preset id、allow 集合与失败原因。";
|
|
44
|
+
readonly 'e-325-host-data-plane-unavailable': "E-325: Host 数据面不可用:没有挂载 connection 服务,面板与审批路由不会注册。改用 /evolution pending、approve、reject 命令面完成同等操作;运行 /evolution doctor 查看详情。";
|
|
42
45
|
}>;
|
|
43
46
|
/** One table key. */
|
|
44
47
|
export type EvolutionErrorId = keyof typeof EVOLUTION_ERRORS;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE error-code table: E-301…E-325 with the exact text each one emits (E-317…E-319 are the
|
|
3
|
+
* write-admission gates' own refusals).
|
|
4
|
+
*
|
|
5
|
+
* The codes used to be spelled out at 33 call sites across two packages, so one
|
|
6
|
+
* code's wording could drift between the branches that answer it and nothing could
|
|
7
|
+
* see the drift. Keys name the SCENARIO rather than the code, because a code
|
|
8
|
+
* legitimately carries more than one text (E-301 answers pending / approve / reject
|
|
9
|
+
* with different guidance); the code still leads every string, so model- and
|
|
10
|
+
* user-facing output is unchanged. Interpolations are written `%aN%` and filled by
|
|
11
|
+
* `errorText` from the values the call site used to interpolate inline.
|
|
12
|
+
* @module @lmzhen/dsh-evolution-core
|
|
13
|
+
*/
|
|
14
|
+
export const EVOLUTION_ERRORS = Object.freeze({
|
|
15
|
+
'e-305-this-invocation-carries-no': 'E-305: 本次调用没有携带 agent —— `%a1%` 需要由会话支撑的调用(请在 GUI 或 CLI 的会话里运行)。',
|
|
16
|
+
'e-306-this-deployment-stages-foreground': 'E-306: 本部署会暂存前台写入,但 `/evolution %a1%` 无法通过技能运行器回放 —— 没有可暂存的内容。请在批准策略为 \'never\' 的会话里运行,或在 evolution-approval 行上设置 `stageForeground: false`,然后重试该命令。',
|
|
17
|
+
'e-301-approval-service-not-mounted': 'E-301: 批准服务未挂载:待批准写入无法列出或回放。挂载 evolution-approval 行(随 evolution-host/evolution-all 提供)后,运行 /evolution doctor 可查看已挂载的服务。',
|
|
18
|
+
'e-301-approval-service-not-mounted-2': 'E-301: 批准服务未挂载。下一步:evolution-approval 行随 evolution-host/evolution-all 一起提供 —— 运行 /evolution doctor 查看已挂载的服务。',
|
|
19
|
+
'e-304-the-mounted-approval-service': 'E-304: 已挂载的批准服务早于本能力发布 —— 请改为驳回该记录。',
|
|
20
|
+
'e-302-curator-service-not-mounted': 'E-302: 策展服务未挂载。下一步:挂载 evolution-curator 行(evolution-host/evolution-all),并运行 /evolution doctor。',
|
|
21
|
+
'e-305-the-invocation-agent-exposes': 'E-305: 调用 agent 既没有暴露 `followup` 也没有 `inject` —— %a1% 没有投递通道。',
|
|
22
|
+
'e-306-this-deployment-stages-foreground-2': 'E-306: 本部署会暂存前台写入,但本次调用没有 agent 会话 —— `/evolution restructure` 会暂存一条没有会话归属的记录。请在 GUI 或 CLI 的会话里运行,或在 `stageForeground: false` 行上设置,然后重试该命令。',
|
|
23
|
+
'e-303-replay-service-not-mounted': 'E-303: 回放服务未挂载。下一步:挂载 evolution-replay 行(evolution-host/evolution-all),并运行 /evolution doctor。',
|
|
24
|
+
'e-307-the-settings-service-could': 'E-307: 设置服务无法报告它的分区(%a1%)。/evolution params 需要用户层来区分覆盖值与部署值;运行 /evolution doctor 查看已挂载的服务。',
|
|
25
|
+
'e-308-unknown-parameter-group-group': 'E-308: 未知的参数分组「%a1%」—— 已知分组:%a2%。',
|
|
26
|
+
'e-311-no-settings-service-is': 'E-311: 没有挂载设置服务,因此没有可写入的用户层。请挂载 settings 行(packages/settings/settings-file)后重试;本命令从不修改 cordis.yml —— 部署值属于部署。/evolution doctor 会列出已挂载的服务。',
|
|
27
|
+
'e-312-error-instanceof-Error-error': 'E-312: %a1% —— /evolution params 会列出规范 id。',
|
|
28
|
+
'e-313-unknown-parameter-id-evolution': 'E-313: 未知参数「%a1%」—— /evolution params 会列出每个已注册 id 及其层级与用户层。',
|
|
29
|
+
'e-314-id-is-a-deployment': 'E-314: 「%a1%」是部署参数(层级 %a2%,归属 %a3%)—— 它写在 cordis.yml 的插件行或策略行上,而不是从会话写入。/evolution params 会显示每一行由谁可写。',
|
|
30
|
+
'e-315-id-has-no-user': 'E-315: 「%a1%」没有用户层 —— 归属 %a2% 没有发布设置命名空间,因此该值留在部署层。',
|
|
31
|
+
'e-307-the-settings-service-could-2': 'E-307: 设置服务无法报告它的分区(%a1%)。/evolution policy set 需要当前修订号才能安全写入;运行 /evolution doctor 查看已挂载的服务。',
|
|
32
|
+
'e-316-namespace-namespace-is-not': 'E-316: namespace "%a1%" 未注册 —— 归属 %a2% 在本组合中未挂载,因此无法从这里写入它的参数。 /evolution params 会把那些行标为 \'unregistered\'.',
|
|
33
|
+
'e-309-revision-conflict-you-sent': 'E-309: 修订冲突 —— 你提交的是 %a1%,文档当前为 %a2%。请用 /evolution params --json 重新读取后重试。',
|
|
34
|
+
'e-309-revision-conflict-message-Re': 'E-309: 修订冲突 —— %a1%。请用 /evolution params --json 重新读取后重试。',
|
|
35
|
+
'e-310-the-settings-service-refused': 'E-310: 设置服务拒绝了这次写入:%a1% —— 归属插件的规则优先(跨字段成对约束,或只能收紧的上限)。/evolution params 会显示当前值。',
|
|
36
|
+
// 0.9.0: the write-admission sequence's own refusals (tool-skill-manage/src/write-gates.ts).
|
|
37
|
+
'e-317-skill-write-not-confirmed': 'E-317: 技能 "%a1%" 未被%a2% —— 确认提示被拒绝或关闭,因此没有写入任何内容。每次创建与裸删除都会出现该提示;只有操作者要求时才重复调用。',
|
|
38
|
+
'e-318-skill-write-without-a-read': 'E-318: 本会话没有读取技能 "%a1%",因此拒绝这次写入。请先用 `skill` 工具读取它(失败的读取不算),然后重复该写入。',
|
|
39
|
+
// 0.12.0: the confirmation's own deadline (`skillWriteConfirm: 'timeout'`).
|
|
40
|
+
'e-320-the-profile-home-is-not': 'E-320: 本宿主没有报告 profile home,因此无法定位旧设置文档。/evolution migrate 读取 `<profile home>/settings.yaml.imported`;请在由 profile 启动的会话里运行。',
|
|
41
|
+
// G5.12/U-4: the cross-field constraint the registry declares (params.ts) has one enforcement
|
|
42
|
+
// point, and this is its message. The card shows the same relation before a save is attempted.
|
|
43
|
+
'e-322-a-parameter-would-break-a': 'E-322: %a1% 至少应为 %a2%,但这次写入把它设成 %a3%。未写入任何内容。',
|
|
44
|
+
'e-321-no-io-provider-is': 'E-321: 没有挂载 evolution-io 提供方,因此无法读取旧设置文档。请挂载 evolution-io 行(evolution-host/evolution-all)后重试。',
|
|
45
|
+
'e-319-skill-write-confirm-timed-out': 'E-319: 技能 "%a1%" 未被%a2% —— 确认提示在 %a3% 秒内无人应答,因此该写入被取消。没有人在场决定:把 `skillWriteConfirm` 设为 `auto` 可免提示直接写入,保留 `ask` 则一直等到有人应答,或提高 `skillWriteConfirmTimeoutSeconds`。',
|
|
46
|
+
'e-323-subagent-channel-unavailable': 'E-323: 子代理通道不可用:缺少 subagents/agentPresets 服务,或 spawn provider 不支持 agentPreset 能力。审阅/维护会明确标记为不可用,不会静默退回 inject。运行 /evolution doctor 查看原因与修复指引。',
|
|
47
|
+
'e-324-restricted-preset-unavailable': 'E-324: 受限 preset 创建失败(读取父 preset、解析、注册或限制器挂载任一步失败):%a1%。审阅/维护通道不可用;运行 /evolution doctor 查看 preset id、allow 集合与失败原因。',
|
|
48
|
+
'e-325-host-data-plane-unavailable': 'E-325: Host 数据面不可用:没有挂载 connection 服务,面板与审批路由不会注册。改用 /evolution pending、approve、reject 命令面完成同等操作;运行 /evolution doctor 查看详情。',
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Render one table entry, substituting its interpolation slots.
|
|
52
|
+
* @param id - the scenario key.
|
|
53
|
+
* @param args - values for the entry's `%aN%` slots.
|
|
54
|
+
* @returns the message text the call site used to spell out inline.
|
|
55
|
+
*/
|
|
56
|
+
export function errorText(id, args = {}) {
|
|
57
|
+
return EVOLUTION_ERRORS[id].replace(/%a(\d+)%/g, (_match, n) => String(args['a' + n] ?? ''));
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local events emitted by the evolution family on the cordis event
|
|
3
|
+
* bus. Consumers subscribe with `ctx.on(...)`; producers dispatch with
|
|
4
|
+
* `ctx.emit(...)`.
|
|
5
|
+
*
|
|
6
|
+
* These are deliberately NOT session events: a persisted session log may only
|
|
7
|
+
* contain types from the host's generated `KNOWN_SESSION_EVENT_TYPES` set —
|
|
8
|
+
* the persistence read path refuses to interpret a log carrying any other
|
|
9
|
+
* type unless the envelope marks it `ignorable`, and `Session.append` offers
|
|
10
|
+
* no channel to write that marker. Appending any `evolution/*` type therefore
|
|
11
|
+
* made the whole session unresumable (A-line P0-1, fixed in rc.42 by moving
|
|
12
|
+
* these events off `session.append`). Plan-outcome durability lives in the
|
|
13
|
+
* evolution-activity store, not the session log.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which session frames are EVIDENCE, and which are bookkeeping.
|
|
3
|
+
*
|
|
4
|
+
* A plan op must cite `evidence` seqs, and the validator's range rule
|
|
5
|
+
* (`evolution-plan-validator`, rule `EVIDENCE_RANGE`) answers only "is this an integer inside
|
|
6
|
+
* the window the plan was authored against". A boundary frame satisfies that while carrying
|
|
7
|
+
* nothing a claim can rest on: the `turn/start` seq is the frame a claim about that turn was
|
|
8
|
+
* most likely read out of, and citing it is indistinguishable from citing a real exchange. This
|
|
9
|
+
* module owns the ONE classification of a session frame by what it CARRIES, so the plan path can
|
|
10
|
+
* report an op whose whole citation list is bookkeeping (design
|
|
11
|
+
* `dsh-evolution-skill-history-design.md` §3D, phase 1: report only — no op is refused here).
|
|
12
|
+
*
|
|
13
|
+
* The classification is a DENY list of the pure bookends, not an allow list of content frames.
|
|
14
|
+
* The platform's own vocabulary (`KNOWN_SESSION_EVENT_TYPES`, 57 members at 0.1.5-rc.2) mixes
|
|
15
|
+
* content frames with state-change frames, and out-of-repo plugins append types the family cannot
|
|
16
|
+
* enumerate; an unknown frame is therefore treated as carrying content, because a report that
|
|
17
|
+
* fires on an unclassifiable frame is a false positive and phase 1 exists to measure how many TRUE
|
|
18
|
+
* ones there are. Tightening — a narrower definition, or an allow list built from the platform's
|
|
19
|
+
* vocabulary — changes this table and nothing else (`evidenceKindIndex` is the only reader).
|
|
20
|
+
* @module @lmzhen/dsh-evolution-core
|
|
21
|
+
*/
|
|
22
|
+
/** The frame types that record a boundary and carry no content: the turn and step bookends. */
|
|
23
|
+
const BOOKKEEPING_FRAME_TYPES = new Set([
|
|
24
|
+
'turn/start',
|
|
25
|
+
'turn/end',
|
|
26
|
+
'step/start',
|
|
27
|
+
'step/end',
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Whether one frame type records a boundary and carries no content.
|
|
31
|
+
*
|
|
32
|
+
* The classification table's SECOND reader: the live projection unit
|
|
33
|
+
* (`session-projection.ts`) folds the same answer per event, so the rule stays one table.
|
|
34
|
+
* @param type - a platform session-event type.
|
|
35
|
+
* @returns true for the turn/step bookends.
|
|
36
|
+
*/
|
|
37
|
+
export function isBookkeepingFrame(type) {
|
|
38
|
+
return BOOKKEEPING_FRAME_TYPES.has(type);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The seqs of the frames that carry content.
|
|
42
|
+
*
|
|
43
|
+
* @param events - the session's frames, oldest first (any iterable; nothing is retained).
|
|
44
|
+
* @returns the substantive seqs, or `undefined` when NO frame carried a usable seq. The two
|
|
45
|
+
* answers are different facts: an empty SET says "every sequenced frame is a boundary", while
|
|
46
|
+
* `undefined` says "this log cannot tell a boundary from a message" (a stub session in a test,
|
|
47
|
+
* a composition whose events carry no seq). A caller that cannot classify must stay silent
|
|
48
|
+
* rather than report every op — the same three-state posture as `sessionReadSkillNames`.
|
|
49
|
+
*/
|
|
50
|
+
export function evidenceKindIndex(events) {
|
|
51
|
+
const substantive = new Set();
|
|
52
|
+
let sequenced = 0;
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
const seq = event.seq;
|
|
55
|
+
// Only an integer seq is citable, so only an integer seq is classifiable.
|
|
56
|
+
if (typeof seq !== 'number' || !Number.isInteger(seq))
|
|
57
|
+
continue;
|
|
58
|
+
sequenced += 1;
|
|
59
|
+
if (typeof event.type === 'string' && BOOKKEEPING_FRAME_TYPES.has(event.type))
|
|
60
|
+
continue;
|
|
61
|
+
substantive.add(seq);
|
|
62
|
+
}
|
|
63
|
+
return sequenced === 0 ? undefined : substantive;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=evidence.js.map
|