@lmzhen/dsh-evolution-core 0.26.15 → 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.
Files changed (116) hide show
  1. package/README.md +16 -1
  2. package/lib/composition/index.js +3 -0
  3. package/lib/composition-D8Y3-ROd.js +604 -0
  4. package/lib/ensure-MEYMj9jn.js +222 -0
  5. package/lib/errors-Br2-uUSK.js +57 -0
  6. package/lib/http-routes-BP-S3KV-.js +149 -0
  7. package/lib/http-routes.js +2 -0
  8. package/lib/index.js +777 -840
  9. package/lib/restrictor.js +37 -0
  10. package/lib/subagent/index.js +186 -0
  11. package/lib/types/citations.js +192 -0
  12. package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +2 -2
  13. package/lib/types/composition/composer.js +681 -0
  14. package/lib/types/composition/ensure.d.ts +10 -0
  15. package/lib/types/composition/ensure.js +95 -0
  16. package/lib/types/composition/index.d.ts +5 -0
  17. package/lib/types/composition/index.js +7 -0
  18. package/lib/types/composition/preset-ids.d.ts +17 -0
  19. package/lib/types/composition/preset-ids.js +21 -0
  20. package/lib/types/composition/restricted-preset.d.ts +36 -0
  21. package/lib/types/composition/restricted-preset.js +42 -0
  22. package/lib/types/constants.js +223 -0
  23. package/lib/types/cost.js +67 -0
  24. package/lib/types/curator.d.ts +2 -0
  25. package/lib/types/curator.js +346 -0
  26. package/lib/types/diagnostics.d.ts +26 -0
  27. package/lib/types/diagnostics.js +23 -0
  28. package/lib/types/document-body.js +91 -0
  29. package/lib/types/drift-signals.js +319 -0
  30. package/lib/types/env.js +25 -0
  31. package/lib/types/errors.d.ts +4 -1
  32. package/lib/types/errors.js +59 -0
  33. package/lib/types/events.js +16 -0
  34. package/lib/types/evidence.js +65 -0
  35. package/lib/types/evolution-events.js +479 -0
  36. package/lib/types/fold-untrusted.js +44 -0
  37. package/lib/types/frontmatter.js +623 -0
  38. package/lib/types/fuzzy-match.js +135 -0
  39. package/lib/types/gates.js +51 -0
  40. package/lib/types/guards.js +27 -0
  41. package/lib/types/health-baseline.js +63 -0
  42. package/lib/types/http-routes.d.ts +67 -35
  43. package/lib/types/http-routes.js +135 -0
  44. package/lib/types/index.d.ts +13 -2
  45. package/lib/types/index.js +101 -0
  46. package/lib/types/instance-scope.js +69 -0
  47. package/lib/types/io.js +675 -0
  48. package/lib/types/learn-prompt.js +42 -0
  49. package/lib/types/limits.js +63 -0
  50. package/lib/types/live.js +107 -0
  51. package/lib/types/long-runs.js +96 -0
  52. package/lib/types/memory-store.d.ts +0 -16
  53. package/lib/types/memory-store.js +606 -0
  54. package/lib/types/mutations.js +95 -0
  55. package/lib/types/namespace-migration.js +156 -0
  56. package/lib/types/numeric.js +47 -0
  57. package/lib/types/opt-in.js +101 -0
  58. package/lib/types/package-identity.d.ts +14 -0
  59. package/lib/types/package-identity.js +47 -0
  60. package/lib/types/params.js +328 -0
  61. package/lib/types/payload.js +36 -0
  62. package/lib/types/platform-line.generated.d.ts +10 -0
  63. package/lib/types/platform-line.generated.js +12 -0
  64. package/lib/types/platform-services.d.ts +2 -2
  65. package/lib/types/platform-services.js +52 -0
  66. package/lib/types/probe.d.ts +9 -3
  67. package/lib/types/probe.js +122 -0
  68. package/lib/types/prompts.js +409 -0
  69. package/lib/types/quality.js +308 -0
  70. package/lib/types/reachability.d.ts +46 -0
  71. package/lib/types/reachability.js +122 -0
  72. package/lib/types/redact.js +257 -0
  73. package/lib/types/reference-rewrite.js +144 -0
  74. package/lib/types/reports.js +110 -0
  75. package/lib/types/restrictor.d.ts +11 -0
  76. package/lib/types/restrictor.js +48 -0
  77. package/lib/types/review-channel.js +59 -0
  78. package/lib/types/run-registry.js +345 -0
  79. package/lib/types/scope.js +7 -0
  80. package/lib/types/serial.js +17 -0
  81. package/lib/types/session-fold.d.ts +15 -0
  82. package/lib/types/session-fold.js +28 -0
  83. package/lib/types/session-projection.js +295 -0
  84. package/lib/types/signals.js +209 -0
  85. package/lib/types/skill-discovery.js +66 -0
  86. package/lib/types/skill-health.js +134 -0
  87. package/lib/types/skill-history.js +745 -0
  88. package/lib/types/skill-lint.js +112 -0
  89. package/lib/types/skill-reads.js +67 -0
  90. package/lib/types/skill-row-cells.js +123 -0
  91. package/lib/types/skill-store.d.ts +34 -3
  92. package/lib/types/skill-store.js +3528 -0
  93. package/lib/types/state-store.js +60 -0
  94. package/lib/types/store-ledger.d.ts +44 -0
  95. package/lib/types/store-ledger.js +117 -0
  96. package/lib/types/subagent/channel.d.ts +4 -0
  97. package/lib/types/subagent/channel.js +131 -0
  98. package/lib/types/subagent/index.d.ts +4 -0
  99. package/lib/types/subagent/index.js +5 -0
  100. package/lib/types/subagent/readiness.d.ts +6 -0
  101. package/lib/types/subagent/readiness.js +35 -0
  102. package/lib/types/subagent/types.d.ts +58 -0
  103. package/lib/types/subagent/types.js +2 -0
  104. package/lib/types/threats.js +338 -0
  105. package/lib/types/tool-dispatch.d.ts +5 -0
  106. package/lib/types/tool-dispatch.js +535 -0
  107. package/lib/types/tree-write.d.ts +53 -0
  108. package/lib/types/tree-write.js +40 -0
  109. package/lib/types/unit-members.js +121 -0
  110. package/lib/types/usage.js +474 -0
  111. package/lib/types/write-authority.d.ts +87 -0
  112. package/lib/types/write-authority.js +23 -0
  113. package/lib/types/write-gates.d.ts +111 -0
  114. package/lib/types/write-gates.js +344 -0
  115. package/lib/types/write-inventory.js +158 -0
  116. package/package.json +39 -11
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Deterministic skill curator: active → stale → archived transitions.
3
+ * Pure function with one deliberate side effect: records in the passed
4
+ * `usage` map are MUTATED (state/archived_at) to carry the transition — the
5
+ * caller owns the map and decides whether to clone first (dry-run) or persist
6
+ * after. File moves are performed by SkillLibrary.
7
+ */
8
+ import { daysSinceIso, idleDays } from "./usage.js";
9
+ import { createGateSet } from "./gates.js";
10
+ import { PROTECTED_BUILTIN_SKILLS, SKILL_NAME_RE } from "./constants.js";
11
+ export { PROTECTED_BUILTIN_SKILLS } from "./constants.js";
12
+ export function buildCuratorRunReport(input) {
13
+ return {
14
+ schemaVersion: 1,
15
+ runId: input.runId,
16
+ startedAt: input.startedAt,
17
+ finishedAt: input.finishedAt,
18
+ staleCandidates: [...input.staleCandidates],
19
+ llmNominations: [...input.llmNominations],
20
+ archiveCandidates: [...input.archiveCandidates],
21
+ archived: [...input.archived],
22
+ failed: [...input.failed],
23
+ ...input.aborted === undefined ? {} : { aborted: input.aborted },
24
+ ...input.unattributed === undefined || input.unattributed.length === 0 ? {} : { unattributed: [...input.unattributed] },
25
+ ...input.consolidated === undefined ? {} : { consolidated: [...input.consolidated] },
26
+ ...input.snapshotPath === undefined ? {} : { snapshotPath: input.snapshotPath },
27
+ ...input.llmReviewEnabled === undefined ? {} : { llmReviewEnabled: input.llmReviewEnabled },
28
+ ...input.nominationsWarnings === undefined ? {} : { nominationsWarnings: [...input.nominationsWarnings] },
29
+ ...input.scanWarnings === undefined ? {} : { scanWarnings: [...input.scanWarnings] },
30
+ ...input.wouldPrune === undefined ? {} : { wouldPrune: input.wouldPrune === null ? null : [...input.wouldPrune] },
31
+ };
32
+ }
33
+ /**
34
+ * Render a curator run report as a compact human-readable markdown digest
35
+ * (G6): run metadata first, then the notable sections (archived / failed /
36
+ * stale candidates / LLM nominations).
37
+ */
38
+ export function renderCuratorReportMarkdown(report) {
39
+ const lines = [
40
+ `# Curator run ${report.runId}`,
41
+ '',
42
+ `- **Started** ${report.startedAt}`,
43
+ `- **Finished** ${report.finishedAt}`,
44
+ `- **Stale candidates**: ${report.staleCandidates.length}`,
45
+ `- **LLM nominations**: ${report.llmNominations.length}`,
46
+ `- **Archived**: ${report.archived.length}`,
47
+ `- **Failed**: ${report.failed.length}`,
48
+ // V27 CUR-2: an aborted run says so on the summary line — the digest is the
49
+ // only artefact an operator reads, and "Failed: 0" over a run that was cut
50
+ // short is the false-clean signal the audit flagged.
51
+ ...report.aborted === undefined ? [] : [`- **Aborted**: ${report.aborted}`],
52
+ ...report.unattributed === undefined || report.unattributed.length === 0
53
+ ? []
54
+ : [`- **Unattributed errors**: ${report.unattributed.length}`],
55
+ ...report.snapshotPath === undefined ? [] : [`- **Snapshot**: ${report.snapshotPath}`],
56
+ ...report.routeStubs === undefined ? [] : [`- **Stale route stubs**: ${report.routeStubs.stubs}`],
57
+ ...report.llmReviewEnabled === undefined ? [] : [`- **llmReview**: ${report.llmReviewEnabled}`],
58
+ ...report.wouldPrune === undefined ? [] : [report.wouldPrune === null
59
+ ? '- **Would prune (archiveRetention=report)**: unknown (archive listing unreadable)'
60
+ : `- **Would prune (archiveRetention=report)**: ${report.wouldPrune.length}`],
61
+ ...report.nominationsWarnings === undefined || report.nominationsWarnings.length === 0 ? [] : [`- **Nomination warnings**: ${report.nominationsWarnings.join('; ')}`],
62
+ ...report.scanWarnings === undefined || report.scanWarnings.length === 0 ? [] : report.scanWarnings.map((warning) => '- **Dedup scan**: ' + warning),
63
+ ];
64
+ const section = (title, items) => items.length === 0 ? [] : ['', `## ${title}`, '', ...items.map(item => `- ${item}`)];
65
+ return [
66
+ ...lines,
67
+ ...section('Archived', report.archived.map(item => `${item.name} (${item.reason})`)),
68
+ ...section('Failed', report.failed.map(item => `${item.name}: ${item.reason}`)),
69
+ ...section('Unattributed', report.unattributed ?? []),
70
+ ...section('Stale route stubs', (report.routeStubs?.detail ?? []).map(stub => stub.skill + ' v' + String(stub.v) + ' -> ' + stub.owner + ' (' + stub.reason + ')')),
71
+ ...section('Stale candidates', report.staleCandidates),
72
+ ...section('LLM nominations', report.llmNominations),
73
+ ...section('Retention (kept, not deleted)', report.wouldPrune ?? []),
74
+ '',
75
+ ].join('\n');
76
+ }
77
+ // C-12 (v10 audit): NOMINATION_NAME_RE was byte-identical to SKILL_NAME_RE —
78
+ // the constant is the single source now, so a future shape change (length
79
+ // ceiling, underscore policy) cannot fork curator parsing from the stores.
80
+ const NOMINATION_NAME_RE = SKILL_NAME_RE;
81
+ /**
82
+ * Parse the curator LLM's YAML nomination block (consolidations + prunings).
83
+ * Line-oriented and lenient by design: the LLM output is advisory, every name
84
+ * is re-validated against the tree before any file move happens downstream.
85
+ */
86
+ export function parseCuratorNominations(text) {
87
+ const prunings = [];
88
+ const consolidations = [];
89
+ const warnings = [];
90
+ let section = null;
91
+ let currentFrom = '';
92
+ let currentMode;
93
+ // S1-C4: an `into:` line (usable or not) already "accounts for" the open
94
+ // entry — a usable one closes it, an unusable one gets its own P1-9 warning.
95
+ // The dangling-entry warning below must stay silent in that case.
96
+ let currentIntoSpoken = false;
97
+ for (const line of text.split('\n')) {
98
+ // V6-35: the YAML section HEADERS are tracked so a `- name:` under the
99
+ // prunings header (the normal shape) is not mistaken for a misplaced one.
100
+ const header = /^\s*(consolidations|prunings)\s*:\s*$/.exec(line);
101
+ if (header) {
102
+ section = header[1] === 'consolidations' ? 'consolidations' : 'prunings';
103
+ continue;
104
+ }
105
+ const consolidated = /^\s*-\s*from:\s*([a-z0-9][a-z0-9-]*)(?:\s*#.*)?\s*$/.exec(line);
106
+ if (consolidated) {
107
+ // S1-C4: an unfinished previous entry (a `- from:` that never reached an
108
+ // `into:`) is silently replaced here — and the same silent drop happens
109
+ // when the output ends mid-entry (maxTokens truncation, the exact
110
+ // V6-35 case the warnings channel was built for). Warn on both paths;
111
+ // the drop itself stays (safe side: fewer merges, no content loss).
112
+ if (currentFrom !== '' && !currentIntoSpoken)
113
+ warnings.push(`"- from: ${currentFrom}" dropped — no "into:" arrived before the next entry`);
114
+ section = 'consolidations';
115
+ currentFrom = consolidated[1] ?? '';
116
+ currentMode = undefined;
117
+ currentIntoSpoken = false;
118
+ continue;
119
+ }
120
+ const mode = /^\s*mode:\s*(append|reference)(?:\s*#.*)?\s*$/.exec(line);
121
+ if (mode) {
122
+ if (currentFrom !== '')
123
+ currentMode = mode[1] === 'reference' ? 'reference' : 'append';
124
+ // V6-35: a `mode:` with no preceding `- from:` (or after the `into:` that
125
+ // closed it) is silently dropped — the consolidation stays 'append'.
126
+ // Warn instead so a mis-formed `mode: reference` (demote intent) is
127
+ // visible in the run report instead of degrading to an append.
128
+ else
129
+ warnings.push(`mode: ${mode[1]} ignored — no preceding "- from:" entry (the consolidation falls back to append)`);
130
+ continue;
131
+ }
132
+ const into = /^\s*into:\s*([a-z0-9][a-z0-9-]*)(?:\s*#.*)?\s*$/.exec(line);
133
+ if (into) {
134
+ const intoName = into[1] ?? '';
135
+ if (section === 'consolidations' && currentFrom !== '' && currentFrom !== intoName) {
136
+ consolidations.push({
137
+ from: currentFrom,
138
+ into: intoName,
139
+ ...currentMode === undefined ? {} : { mode: currentMode },
140
+ });
141
+ }
142
+ currentFrom = '';
143
+ currentMode = undefined;
144
+ currentIntoSpoken = false;
145
+ continue;
146
+ }
147
+ const pruned = /^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)(?:\s*#.*)?\s*$/.exec(line);
148
+ if (pruned) {
149
+ // V6-35: a `- name:` inside the consolidations section flips the parse
150
+ // to prunings — every later entry is re-read as a pruning. Warn so the
151
+ // mis-placement is visible; parsing stays lenient.
152
+ if (section === 'consolidations')
153
+ warnings.push('"- name:" inside the consolidations section flips the parse to prunings');
154
+ section = 'prunings';
155
+ const name = pruned[1];
156
+ if (name)
157
+ prunings.push(name);
158
+ continue;
159
+ }
160
+ // P1-9 (v38): an anchor KEY with an unusable value (a name outside the
161
+ // charset, an unknown mode) used to vanish without a trace - and `- name:`
162
+ // is the LLM channel's only archive path. Say it instead.
163
+ if (/^\s*(?:-\s*(?:from|name)\s*:|(?:into|mode)\s*:)/.test(line)) {
164
+ // S1-C4: an unusable `into:` line still ACCOUNTS for the open entry (the
165
+ // attempted close is visible via this warning), so the dangling-entry
166
+ // warning below stays silent for it.
167
+ if (currentFrom !== '' && /^\s*into\s*:/.test(line))
168
+ currentIntoSpoken = true;
169
+ warnings.push(`"${line.trim()}" ignored - not a usable nomination line (names are lowercase letters, digits and hyphens; one optional trailing "# comment" is allowed)`);
170
+ }
171
+ }
172
+ // S1-C4: output ended mid-entry (maxTokens truncation or a dropped tail) —
173
+ // the pending consolidation never reached an `into:` and would otherwise
174
+ // vanish without a trace in the run report. An `into:` line that already
175
+ // spoke for the entry (usable closes it; unusable warned via P1-9) does not
176
+ // warn twice.
177
+ if (currentFrom !== '' && !currentIntoSpoken)
178
+ warnings.push(`"- from: ${currentFrom}" dropped — the nomination output ended before its "into:"`);
179
+ const valid = (name) => NOMINATION_NAME_RE.test(name);
180
+ return {
181
+ prunings: prunings.filter(valid),
182
+ consolidations: consolidations.filter(item => valid(item.from) && valid(item.into)),
183
+ warnings,
184
+ };
185
+ }
186
+ /**
187
+ * The lifecycle-candidate gate, shared by the transition engine and the scope
188
+ * view so the two can never disagree: records failing ANY of these gates are
189
+ * outside the managed scope.
190
+ */
191
+ export function lifecycleCandidate(name, record, config, bundled, gates = createGateSet(config),
192
+ // V8-14 (0.3.47): marker-protected names are NOT candidates — the scope view
193
+ // bucketed them as protected but the transition engine still treated an
194
+ // agent-created marker skill as managed (it appeared in managed[] and
195
+ // protected[] at once, and the engine produced a failed archive step that
196
+ // deleteProtection then refused).
197
+ protectedNames) {
198
+ if (record.pinned)
199
+ return false;
200
+ if (protectedNames?.has(name) === true)
201
+ return false;
202
+ // One shared GateSet answers exclude / referenced / suppressed and the
203
+ // protected-builtin list (decision B) - identical verdicts to the former
204
+ // three inline set checks plus the builtin check.
205
+ if (gates.isBlocked(name))
206
+ return false;
207
+ const managed = record.created_by === 'agent' || config.manageUnmanaged === true;
208
+ if (!managed && !(config.pruneBuiltins === true && bundled))
209
+ return false;
210
+ if (record.state === 'archived')
211
+ return false;
212
+ return true;
213
+ }
214
+ /**
215
+ * Read-only scope classification, derived from the SAME gate the transition
216
+ * engine uses (`lifecycleCandidate`), so the view always predicts what a
217
+ * curator pass may touch. `protectedNames` carries the marker info the usage
218
+ * records lack (bundled / hub-installed / pinned from `SkillLibrary.list()`).
219
+ */
220
+ export function computeScopeView(usage, config, protectedNames, gates) {
221
+ const managed = [];
222
+ const watched = [];
223
+ const qualityWarned = [];
224
+ const exempted = [];
225
+ const protectedSet = new Set();
226
+ const gateSet = gates ?? createGateSet(config);
227
+ for (const [name, record] of usage) {
228
+ // Bucket semantics are view-specific and unchanged: exclude/referenced
229
+ // read as exempted, suppressed as protected (decision B shares the SETS,
230
+ // not the presentation).
231
+ if (gateSet.exclude.has(name) || gateSet.referenced.has(name)) {
232
+ exempted.push(name);
233
+ continue;
234
+ }
235
+ const bundled = config.bundledNames?.has(name) === true;
236
+ const suppressed = gateSet.suppressed.has(name);
237
+ // V6-36 (0.3.36): a builtin (PROTECTED_BUILTIN_SKILLS — e.g. 'plan') is
238
+ // blocked by the shared gate, so it lands in NO bucket — the view lost an
239
+ // explanation dimension for the skills it must never touch. Bucket it as
240
+ // protected so the view always predicts what a curator pass may touch.
241
+ const isBuiltin = PROTECTED_BUILTIN_SKILLS.has(name);
242
+ if (record.pinned || bundled || suppressed || protectedNames?.has(name) === true || isBuiltin)
243
+ protectedSet.add(name);
244
+ if (lifecycleCandidate(name, record, config, bundled, gateSet, protectedNames)) {
245
+ managed.push(name);
246
+ // P1-1 (v15): the warn buckets read the UNION of the curator six-factor
247
+ // pair and the feedback pair (same union as the lifecycle engine's
248
+ // stale-window decision, so the view predicts the engine).
249
+ const warned = record.quality_warn === true || record.feedback_warn === true;
250
+ if (record.state === 'stale' || warned)
251
+ watched.push(name);
252
+ if (warned)
253
+ qualityWarned.push(name);
254
+ }
255
+ }
256
+ return {
257
+ managed: managed.sort(),
258
+ watched: watched.sort(),
259
+ qualityWarned: qualityWarned.sort(),
260
+ exempted: exempted.sort(),
261
+ protected: [...protectedSet].sort(),
262
+ };
263
+ }
264
+ export function computeLifecycleTransitions(usage, config, now = new Date(), gates,
265
+ // V8-14 (0.3.47): marker-protected names (bundled / hub-installed / pinned
266
+ // markers from SkillLibrary.list()) never enter the transition engine — the
267
+ // scope view already buckets them as protected; passing the same set keeps
268
+ // the two derivations in agreement.
269
+ protectedNames) {
270
+ const result = { transitions: [], archive: [], reactivate: [], markStale: [] };
271
+ // One GateSet per run (decision B): callers holding a shared instance pass
272
+ // it in so the lifecycle engine and the merge gates can never disagree.
273
+ const gateSet = gates ?? createGateSet(config);
274
+ for (const [name, record] of usage) {
275
+ const bundled = config.bundledNames?.has(name) === true;
276
+ if (!lifecycleCandidate(name, record, config, bundled, gateSet, protectedNames))
277
+ continue;
278
+ const age = daysSinceIso(null, record.created_at, now.getTime());
279
+ // P1-1 (v15): the warn state is the UNION of the curator-owned six-factor
280
+ // pair and the feedback-owned pair — negative feedback must shorten the
281
+ // stale window (that is the feedback package's entire advertised purpose).
282
+ // Field ownership: see `UsageRecord` in usage.ts.
283
+ const qualityWarn = record.quality_warn === true || record.feedback_warn === true;
284
+ // P2-12/E-2 (v18): "never used" means never LOADED — `view_count` is the
285
+ // in-tree signal (the platform `skill` tool) and `use_count` an external
286
+ // host signal, so the guard reads both. It used to read `use_count` alone
287
+ // and therefore deferred EVERY skill younger than staleAfterDays,
288
+ // including a quality/feedback-warned one whose short window is exactly
289
+ // what the union read exists for. Warned skills skip the deferral and enter
290
+ // the normal (short-window) decision; the scope view lists them as watched.
291
+ if (record.use_count + record.view_count === 0 && !qualityWarn && age < config.staleAfterDays)
292
+ continue;
293
+ const idle = idleDays(record, now);
294
+ const staleAfterDays = qualityWarn && config.qualityWarnStaleAfterDays !== undefined
295
+ ? config.qualityWarnStaleAfterDays
296
+ : config.staleAfterDays;
297
+ if (record.state === 'active') {
298
+ if (idle >= config.archiveAfterDays) {
299
+ record.state = 'archived';
300
+ record.archived_at = now.toISOString();
301
+ result.transitions.push({ name, from: 'active', to: 'archived', reason: `idle ${Math.round(idle)}d >= ${config.archiveAfterDays}d` });
302
+ result.archive.push(name);
303
+ }
304
+ else if (idle >= staleAfterDays) {
305
+ record.state = 'stale';
306
+ // P3 (v16/v17): attribute the warn source so a curator report can tell
307
+ // a user's negative feedback apart from the six-factor score. Tie-break
308
+ // (both flags true): FEEDBACK wins — it is the more informative,
309
+ // user-visible signal; both sources apply the same short window, so
310
+ // this is report attribution only.
311
+ const warnSource = record.feedback_warn === true
312
+ ? 'feedback-warn stale'
313
+ : 'quality-warn stale';
314
+ const reason = qualityWarn
315
+ ? `idle ${Math.round(idle)}d >= ${warnSource} ${staleAfterDays}d`
316
+ : `idle ${Math.round(idle)}d >= ${staleAfterDays}d`;
317
+ result.transitions.push({ name, from: 'active', to: 'stale', reason });
318
+ result.markStale.push(name);
319
+ }
320
+ }
321
+ else {
322
+ // V27 CC-4: the archive bound is checked FIRST here, mirroring the active
323
+ // branch above. The old order tested `idle < staleAfterDays` first, so a
324
+ // contradictory configuration (warn window longer than the archive
325
+ // window, e.g. qualityWarnStaleAfterDays=60 with archiveAfterDays=45)
326
+ // REACTIVATED a stale skill at idle=50d instead of archiving it — the two
327
+ // branches disagreed about the same idle value, and the pair is not
328
+ // validated anywhere. With the archive bound first, the harder bound
329
+ // always wins, and the behavior is otherwise unchanged (for
330
+ // archiveAfterDays >= staleAfterDays the two orders are equivalent).
331
+ if (idle >= config.archiveAfterDays) {
332
+ record.state = 'archived';
333
+ record.archived_at = now.toISOString();
334
+ result.transitions.push({ name, from: 'stale', to: 'archived', reason: `idle ${Math.round(idle)}d >= ${config.archiveAfterDays}d` });
335
+ result.archive.push(name);
336
+ }
337
+ else if (idle < staleAfterDays) {
338
+ record.state = 'active';
339
+ result.transitions.push({ name, from: 'stale', to: 'active', reason: `recent activity ${Math.round(idle)}d` });
340
+ result.reactivate.push(name);
341
+ }
342
+ }
343
+ }
344
+ return result;
345
+ }
346
+ //# sourceMappingURL=curator.js.map
@@ -0,0 +1,26 @@
1
+ export declare const DIAGNOSTICS_VIEW_SCHEMA_VERSION: 2;
2
+ export declare const DIAGNOSTICS_FACT_KEYS: readonly ["platform-line", "family-version", "install-form", "preset-ids", "readonly-contract", "upgrade-order", "fallback-map"];
3
+ export declare const DIAGNOSTICS_RUNTIME_KEYS: readonly ["subagent-channel", "restricted-preset", "host-surface", "session-fold", "platform-services", "runs", "platform-pairing", "install-form-detected"];
4
+ export type DiagnosticsFactKey = (typeof DIAGNOSTICS_FACT_KEYS)[number];
5
+ export type DiagnosticsRuntimeKey = (typeof DIAGNOSTICS_RUNTIME_KEYS)[number];
6
+ export type DiagnosticStatus = 'ok' | 'warn' | 'fail' | 'unknown';
7
+ export interface DocFactRow {
8
+ readonly key: DiagnosticsFactKey;
9
+ readonly label: string;
10
+ readonly value: string;
11
+ readonly home: string;
12
+ }
13
+ export interface DiagnosticRow {
14
+ readonly key: DiagnosticsRuntimeKey;
15
+ readonly label: string;
16
+ readonly status: DiagnosticStatus;
17
+ readonly detail?: string;
18
+ readonly remedy?: string;
19
+ }
20
+ export interface DiagnosticsView {
21
+ readonly schemaVersion: typeof DIAGNOSTICS_VIEW_SCHEMA_VERSION;
22
+ readonly generatedAt: string;
23
+ readonly facts: readonly DocFactRow[];
24
+ readonly runtime: readonly DiagnosticRow[];
25
+ }
26
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1,23 @@
1
+ // DiagnosticsView contract: the ONE home of the doctor key set (W8 builds the view, W7 mirrors
2
+ // it type-only into clients, CLI and panels read the same keys).
3
+ export const DIAGNOSTICS_VIEW_SCHEMA_VERSION = 2;
4
+ export const DIAGNOSTICS_FACT_KEYS = [
5
+ 'platform-line',
6
+ 'family-version',
7
+ 'install-form',
8
+ 'preset-ids',
9
+ 'readonly-contract',
10
+ 'upgrade-order',
11
+ 'fallback-map',
12
+ ];
13
+ export const DIAGNOSTICS_RUNTIME_KEYS = [
14
+ 'subagent-channel',
15
+ 'restricted-preset',
16
+ 'host-surface',
17
+ 'session-fold',
18
+ 'platform-services',
19
+ 'runs',
20
+ 'platform-pairing',
21
+ 'install-form-detected',
22
+ ];
23
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * What the panel shows when it renders one version as a document.
3
+ *
4
+ * Two decisions, both made here so the preview and the rendered diff cannot drift apart:
5
+ *
6
+ * - FRONTMATTER IS NOT DOCUMENT TEXT. A SKILL.md opens with a YAML block; the renderer read its
7
+ * closing `---` as a setext underline, so the metadata line became the document's biggest heading
8
+ * and the real title sat below it (measured on the installed panel 2026-09-28). The block is
9
+ * stripped for DISPLAY only: the raw view and the body route still carry every byte, and the
10
+ * panel already shows the name and description on the row and in the left column.
11
+ * - STRIPPING FAILS OPEN. Only a block that the family's own reader accepts is removed. A document
12
+ * whose text merely starts with a thematic break, a block with no closing line, or a frontmatter
13
+ * whose body is empty is returned UNCHANGED — showing a document's first line is always better
14
+ * than showing nothing.
15
+ *
16
+ * `readingStats` exists for the other half of the reader's honesty: a preview that stops early has
17
+ * to be able to say HOW much it is not showing.
18
+ * @module @lmzhen/dsh-evolution-core/document-body
19
+ */
20
+ import { frontmatterBlock, parseFrontmatter } from "./frontmatter.js";
21
+ /**
22
+ * The document a reader sees: the body without its leading frontmatter block.
23
+ * @param content - the stored bytes of one version.
24
+ * @returns the text to render; unchanged when there is no frontmatter the reader accepts.
25
+ */
26
+ export function displayBodyOf(content) {
27
+ const block = frontmatterBlock(content);
28
+ if (block === null)
29
+ return content;
30
+ // The reader's verdict, not a second parser: a block it refuses is left alone.
31
+ if (parseFrontmatter(content) === null)
32
+ return content;
33
+ const body = block.lines.slice(block.end + 1).join('\n');
34
+ // One leading blank line separates the block from the body; the document starts at its text.
35
+ return body.replace(/^\s*\n/, '');
36
+ }
37
+ /**
38
+ * How much of a version the panel is showing.
39
+ * @param full - the whole stored text.
40
+ * @param shown - the part the route handed over.
41
+ * @returns the counts, so the panel can say what it is not showing.
42
+ */
43
+ export function readingStats(full, shown) {
44
+ return { totalChars: full.length, shownChars: shown.length, truncated: shown.length < full.length };
45
+ }
46
+ /**
47
+ * The H2 sections of a body, in document order — the ONE scan (G3).
48
+ *
49
+ * Both the restructure candidate list and the store anchor validation call this function, so a
50
+ * candidate can only name a heading that exists EXACTLY ONCE and has content: the two rejections a
51
+ * hand-written anchor used to hit ("no such heading", "ambiguous anchor") stop being reachable.
52
+ * H2 is the section boundary (a section spans to the next H2 or EOF), matching the store.
53
+ * @param body - the markdown after the frontmatter block.
54
+ * @returns one row per H2 heading, with `count` filled in a second pass.
55
+ */
56
+ export function h2Sections(body) {
57
+ const lines = body.split(String.fromCharCode(10));
58
+ const rows = [];
59
+ for (let i = 0; i < lines.length; i += 1) {
60
+ const match = /^#{2}\s+(.+?)\s*$/.exec(lines[i] ?? '');
61
+ if (match === null)
62
+ continue;
63
+ const title = (match[1] ?? '').trim();
64
+ let end = lines.length;
65
+ for (let j = i + 1; j < lines.length; j += 1) {
66
+ if (/^#{2}\s/.test(lines[j] ?? '')) {
67
+ end = j;
68
+ break;
69
+ }
70
+ }
71
+ rows.push({ title, start: i, end, count: 0 });
72
+ }
73
+ for (const row of rows)
74
+ row.count = rows.filter(other => other.title === row.title).length;
75
+ return rows;
76
+ }
77
+ /**
78
+ * The sections a restructure proposal may name: heading unique, section non-empty. Derived from
79
+ * `h2Sections`, i.e. from the SAME scan the store validates against.
80
+ * @param body - the markdown after the frontmatter block.
81
+ * @returns move-ready pairs in document order; empty when nothing qualifies.
82
+ */
83
+ export function restructureCandidates(body) {
84
+ return h2Sections(body)
85
+ .filter(section => section.count === 1 && section.end > section.start + 1)
86
+ .map(section => ({
87
+ heading: section.title,
88
+ toFile: 'references/' + section.title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, '') + '.md',
89
+ }));
90
+ }
91
+ //# sourceMappingURL=document-body.js.map