@lincyaw/kernel 0.0.0-stage → 1.0.0-rc.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.
@@ -0,0 +1,285 @@
1
+ import type { AgentTool } from './contracts/agent.ts'
2
+ import type { EnvironmentProvider, EnvironmentSpec, Executor } from './contracts/execution.ts'
3
+ import type { NodeServiceContext, ServerContext, ServerContribution } from './contracts/server.ts'
4
+ import { defineService } from './contracts/service.ts'
5
+ import type { ServiceToken } from './contracts/service.ts'
6
+ import type { DirectiveSpec, IRDocument, IRPass, Renderer, RendererContribution, RoleSpec } from './ir.ts'
7
+
8
+ export * from './contracts/agent.ts'
9
+ export * from './contracts/execution.ts'
10
+ export * from './contracts/operations.ts'
11
+ export * from './contracts/preview.ts'
12
+ export * from './contracts/server.ts'
13
+ export * from './contracts/service.ts'
14
+ export * from './contracts/speech.ts'
15
+
16
+ export const KERNEL_VERSION = '1.0.0'
17
+
18
+ export type Capability = 'fs' | 'network' | 'exec' | 'git'
19
+
20
+ export interface ServiceRequirement {
21
+ token: ServiceToken<unknown>
22
+ optional?: boolean
23
+ }
24
+
25
+ export interface PluginManifest {
26
+ id: string
27
+ version: string
28
+ kernel: string
29
+ provides?: ServiceToken<unknown>[]
30
+ requires?: ServiceRequirement[]
31
+ capabilities?: Capability[]
32
+ }
33
+
34
+ export type PresentationMode = 'slides' | 'document'
35
+
36
+ export interface Logger {
37
+ debug(...args: unknown[]): void
38
+ info(...args: unknown[]): void
39
+ warn(...args: unknown[]): void
40
+ error(...args: unknown[]): void
41
+ }
42
+
43
+ export type Unsubscribe = () => void
44
+
45
+ export interface DomainEvent<Type extends string = string, Payload = unknown> {
46
+ type: Type
47
+ payload: Payload
48
+ }
49
+
50
+ export interface EventBus {
51
+ emit(event: DomainEvent): void
52
+ on<E extends DomainEvent>(type: E['type'], handler: (event: E) => void): Unsubscribe
53
+ }
54
+
55
+ export interface PluginContext {
56
+ config: Record<string, unknown>
57
+ mode: PresentationMode
58
+ provide<T>(token: ServiceToken<T>, impl: T): void
59
+ get<T>(token: ServiceToken<T>): T
60
+ tryGet<T>(token: ServiceToken<T>): T | undefined
61
+ events: EventBus
62
+ log: Logger
63
+ environments?: Readonly<Record<string, EnvironmentSpec>>
64
+ }
65
+
66
+ export interface PluginDefinition {
67
+ manifest: PluginManifest
68
+ setup(ctx: PluginContext): void | Promise<void>
69
+ dispose?(): void | Promise<void>
70
+ }
71
+
72
+ export type PluginStatus = 'started' | 'degraded' | 'disabled' | 'failed'
73
+
74
+ export interface PluginReport {
75
+ id: string
76
+ status: PluginStatus
77
+ reason?: string
78
+ missing?: string[]
79
+ }
80
+
81
+ export interface StartReport {
82
+ order: string[]
83
+ plugins: PluginReport[]
84
+ cycles: string[][]
85
+ }
86
+
87
+ export type KernelEvent =
88
+ | DomainEvent<'kernel.plugin.started', { id: string }>
89
+ | DomainEvent<'kernel.plugin.degraded', { id: string; missing: string[] }>
90
+ | DomainEvent<'kernel.plugin.disabled', { id: string; reason: string }>
91
+ | DomainEvent<'kernel.plugin.failed', { id: string; error: string }>
92
+
93
+ export type Locale = 'zh-CN' | 'en'
94
+
95
+ export const DEFAULT_LANG = 'zh-CN'
96
+
97
+ export function localeOf(lang: string | null | undefined): Locale | undefined {
98
+ const tag = lang?.trim().toLowerCase() ?? ''
99
+ if (tag === 'zh' || tag.startsWith('zh-')) return 'zh-CN'
100
+ if (tag === 'en' || tag.startsWith('en-')) return 'en'
101
+ return undefined
102
+ }
103
+
104
+ export function deckLocale(lang?: string): Locale {
105
+ return localeOf(lang || DEFAULT_LANG) ?? 'en'
106
+ }
107
+
108
+ export interface StepRange {
109
+ from: number
110
+ to?: number
111
+ }
112
+
113
+ export interface StepState {
114
+ active: boolean
115
+ reached: boolean
116
+ done: boolean
117
+ }
118
+
119
+ export function stepStateAt(range: StepRange, step: number): StepState {
120
+ const reached = step >= range.from
121
+ const done = range.to !== undefined && step >= range.to
122
+ return { active: reached && !done, reached, done }
123
+ }
124
+
125
+ export interface Cursor {
126
+ slide: number
127
+ step: number
128
+ }
129
+
130
+ export type AnchorString = `${string}:${string}`
131
+
132
+ export type ResolutionStatus = 'exact' | 'moved' | 'fuzzy' | 'lost'
133
+
134
+ export interface ResolvedLocation {
135
+ file: string
136
+ startLine: number
137
+ endLine: number
138
+ text: string
139
+ }
140
+
141
+ export interface Resolution {
142
+ status: ResolutionStatus
143
+ location?: ResolvedLocation
144
+ fingerprint?: string
145
+ candidates?: ResolvedLocation[]
146
+ message?: string
147
+ }
148
+
149
+ export interface SourceReader {
150
+ read(file: string, ref?: string): Promise<string | undefined>
151
+ }
152
+
153
+ export interface RecordedAnchor {
154
+ fingerprint: string
155
+ file: string
156
+ startLine: number
157
+ }
158
+
159
+ export interface AnchorResolver {
160
+ scheme: string
161
+ resolve(anchor: AnchorString, src: SourceReader, recorded?: RecordedAnchor, ref?: string): Promise<Resolution>
162
+ }
163
+
164
+ export interface AnchorResolverRegistry {
165
+ register(resolver: AnchorResolver): Unsubscribe
166
+ resolve(anchor: AnchorString, src: SourceReader, recorded?: RecordedAnchor, ref?: string): Promise<Resolution>
167
+ schemes(): string[]
168
+ }
169
+
170
+ export interface AnchorLockFile {
171
+ version: 1
172
+ anchors: Record<string, RecordedAnchor>
173
+ }
174
+
175
+ export function isRenderable(resolution: Resolution): boolean {
176
+ return resolution.status === 'exact' || resolution.status === 'moved'
177
+ }
178
+
179
+ export type Severity = 'error' | 'warning' | 'info'
180
+
181
+ export interface Finding {
182
+ checkId: string
183
+ severity: Severity
184
+ message: string
185
+ source?: { file: string; line?: number }
186
+ hint?: string
187
+ }
188
+
189
+ export interface CheckContext {
190
+ deckDir: string
191
+ workspaceRoot: string
192
+ headmatter: Record<string, unknown>
193
+ src: SourceReader
194
+ resolvers: AnchorResolverRegistry
195
+ lock: AnchorLockFile
196
+ ir: IRDocument
197
+ service?<T>(token: ServiceToken<T>): T | undefined
198
+ }
199
+
200
+ export interface Check {
201
+ id: string
202
+ run(ctx: CheckContext): Promise<Finding[]>
203
+ }
204
+
205
+ export interface PresmiumNodePlugin {
206
+ id: string
207
+ version?: string
208
+ kernel?: string
209
+ resolvers?: AnchorResolver[]
210
+ checks?: Check[]
211
+ anchors?(ctx: CheckContext): AnchorString[] | Promise<AnchorString[]>
212
+ directives?: DirectiveSpec[]
213
+ passes?: IRPass[]
214
+ executors?: Executor[]
215
+ renderer?: Renderer
216
+ rendererExtensions?: Record<string, RendererContribution>
217
+ vitePlugins?(ctx: { deckDir: string, workspaceRoot: string, config: Record<string, unknown> }): unknown[]
218
+ services?(ctx: NodeServiceContext): [ServiceToken<unknown>, unknown][]
219
+ environments?: Record<string, EnvironmentProvider>
220
+ server?(ctx: ServerContext): ServerContribution | Promise<ServerContribution>
221
+ roles?: RoleSpec[]
222
+ tools?: AgentTool[]
223
+ operator?: boolean
224
+ }
225
+
226
+ export type Role = 'presenter' | 'learner'
227
+
228
+ export interface Participant {
229
+ id: string
230
+ name: string
231
+ role: Role
232
+ }
233
+
234
+ export type SessionStatus = 'idle' | 'active' | 'ended'
235
+
236
+ export interface SessionSnapshot {
237
+ sessionId?: string
238
+ status: SessionStatus
239
+ me?: Participant
240
+ participants: Participant[]
241
+ presenterCursor?: Cursor
242
+ }
243
+
244
+ export interface SessionService {
245
+ snapshot(): SessionSnapshot
246
+ subscribe(listener: (snapshot: SessionSnapshot) => void): Unsubscribe
247
+ create(name: string): Promise<string>
248
+ join(sessionId: string, name: string): Promise<void>
249
+ end(): Promise<void>
250
+ leave(): void
251
+ archive?(): Promise<string>
252
+ readonly personal?: ClassroomStore
253
+ }
254
+
255
+ export const SESSION = defineService<SessionService>('presmium.session')
256
+
257
+ export interface StoredItem<T> {
258
+ id: string
259
+ value: T
260
+ }
261
+
262
+ export interface ClassroomStore {
263
+ list<T>(collection: string): StoredItem<T>[]
264
+ get<T>(collection: string, id: string): T | undefined
265
+ put<T>(collection: string, id: string, value: T): void
266
+ remove(collection: string, id: string): void
267
+ subscribe(collection: string, listener: () => void): Unsubscribe
268
+ }
269
+
270
+ export const CLASSROOM_STORE = defineService<ClassroomStore>('presmium.classroomStore')
271
+
272
+ export interface PresenterPanel {
273
+ id: string
274
+ title: string
275
+ component: unknown
276
+ order?: number
277
+ }
278
+
279
+ export interface PresenterPanels {
280
+ register(panel: PresenterPanel): Unsubscribe
281
+ list(): PresenterPanel[]
282
+ subscribe(listener: () => void): Unsubscribe
283
+ }
284
+
285
+ export const PRESENTER_PANELS = defineService<PresenterPanels>('presmium.presenterPanels')
package/src/errors.ts ADDED
@@ -0,0 +1,7 @@
1
+ export function errorMessage(error: unknown): string {
2
+ return error instanceof Error ? error.message : String(error)
3
+ }
4
+
5
+ export function isRecord(value: unknown): value is Record<string, unknown> {
6
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
7
+ }
package/src/events.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { DomainEvent, EventBus } from './contracts.ts'
2
+
3
+ type Handler = (event: DomainEvent) => void
4
+ type ErrorHandler = (error: unknown, event: DomainEvent) => void
5
+
6
+ const reportToConsole: ErrorHandler = (error, event) => {
7
+ console.error(`[presmium] handler for "${event.type}" threw`, error)
8
+ }
9
+
10
+ export function createEventBus(onError: ErrorHandler = reportToConsole): EventBus {
11
+ const handlers = new Map<string, Set<Handler>>()
12
+ return {
13
+ emit(event) {
14
+ for (const handler of [...(handlers.get(event.type) ?? [])]) {
15
+ try {
16
+ handler(event)
17
+ } catch (error) {
18
+ onError(error, event)
19
+ }
20
+ }
21
+ },
22
+ on(type, handler) {
23
+ const set = handlers.get(type) ?? new Set()
24
+ handlers.set(type, set)
25
+ const h = handler as Handler
26
+ set.add(h)
27
+ return () => {
28
+ set.delete(h)
29
+ }
30
+ },
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ import type { RunResult } from './contracts.ts'
2
+
3
+ const ALIASES: Record<string, string> = {
4
+ py: 'python',
5
+ python3: 'python',
6
+ js: 'javascript',
7
+ mjs: 'javascript',
8
+ ts: 'typescript',
9
+ }
10
+
11
+ export function languageId(lang: string): string {
12
+ const id = lang.trim().toLowerCase()
13
+ if (id.startsWith('ipython')) return 'python'
14
+ return ALIASES[id] ?? id
15
+ }
16
+
17
+ export function failedRun(executorId: string, stderr: string, durationMs = 0): RunResult {
18
+ return { ok: false, stdout: '', stderr, exitCode: null, durationMs, timedOut: false, source: 'live', executorId }
19
+ }
20
+
21
+ export const CREDENTIAL_VARIABLE = /_(?:KEY|TOKEN|SECRET|PASSWORD|APPID)$/i
@@ -0,0 +1,31 @@
1
+ export function normalizeWhitespace(text: string): string {
2
+ const lines = text
3
+ .replace(/\r\n?/g, '\n')
4
+ .split('\n')
5
+ .map(line => line.replace(/\s+$/u, '').replace(/^[ \t]+/, lead => ' '.repeat(lead.replace(/\t/g, ' ').length)))
6
+ .filter(line => line.length > 0)
7
+ if (lines.length === 0) return ''
8
+ const indent = Math.min(...lines.map(line => line.length - line.trimStart().length))
9
+ return lines
10
+ .map((line) => {
11
+ const depth = line.length - line.trimStart().length - indent
12
+ const body = line.trimStart().replace(/\s+/gu, ' ').replace(/ ?([^\p{L}\p{N}_ ]) ?/gu, '$1')
13
+ return ' '.repeat(depth) + body
14
+ })
15
+ .join('\n')
16
+ }
17
+
18
+ const FNV_OFFSET = 0xcbf29ce484222325n
19
+ const FNV_PRIME = 0x100000001b3n
20
+
21
+ export function fnv1a64(text: string): string {
22
+ let hash = FNV_OFFSET
23
+ for (const byte of new TextEncoder().encode(text)) {
24
+ hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * FNV_PRIME)
25
+ }
26
+ return hash.toString(16).padStart(16, '0')
27
+ }
28
+
29
+ export function fingerprint(text: string): string {
30
+ return fnv1a64(normalizeWhitespace(text))
31
+ }
package/src/follow.ts ADDED
@@ -0,0 +1,37 @@
1
+ import type { IRBlock, IRSection } from './ir.ts'
2
+
3
+ export type StepKeys = string[][]
4
+
5
+ export function stepKeys(section: Pick<IRSection, 'blocks' | 'steps'>, renderer?: string): StepKeys {
6
+ const keys: StepKeys = Array.from({ length: section.steps + 1 }, () => [])
7
+ const add = (step: number, key: string) => step > 0 && keys[step]?.push(key)
8
+ let left = false
9
+ const visit = (blocks: readonly IRBlock[], prefix: string) => {
10
+ for (const block of blocks) {
11
+ if (renderer !== undefined && block.only && !block.only.includes(renderer)) {
12
+ left = true
13
+ continue
14
+ }
15
+ const id = prefix + block.id
16
+ if (block.role === 'visual') {
17
+ for (let k = 0; k <= (block.span ?? 0); k++) add(block.at + k, k === 0 ? id : `${id}+${k}`)
18
+ if (block.until !== undefined) add(block.until, `${id}-`)
19
+ }
20
+ if (block.node.type === 'component' && block.node.children) visit(block.node.children, `${id}/`)
21
+ }
22
+ }
23
+ visit(section.blocks, '')
24
+ return left ? keys.filter((step, i) => i === 0 || step.length > 0) : keys
25
+ }
26
+
27
+ export function stepTrail(keys: StepKeys, step: number): string[] {
28
+ return keys.slice(1, step + 1).flat()
29
+ }
30
+
31
+ export function trailStep(keys: StepKeys, trail: readonly string[]): number {
32
+ const reached = new Set(trail)
33
+ for (let step = keys.length - 1; step > 0; step--) {
34
+ if (keys[step]!.some(key => reached.has(key))) return step
35
+ }
36
+ return 0
37
+ }
package/src/git.ts ADDED
@@ -0,0 +1,12 @@
1
+ export const GIT_SAFE_CONFIG: readonly string[] = ['core.hooksPath=/dev/null', 'core.fsmonitor=false', 'core.quotePath=false', 'commit.gpgSign=false', 'tag.gpgSign=false']
2
+
3
+ export function gitConfigArgs(): string[] {
4
+ return GIT_SAFE_CONFIG.flatMap(entry => ['-c', entry])
5
+ }
6
+
7
+ export function gitEnvironment(env: Record<string, string | undefined>): Record<string, string> {
8
+ const own: Record<string, string> = {}
9
+ for (const [name, value] of Object.entries(env)) if (value !== undefined && !/^GIT_/i.test(name)) own[name] = value
10
+ own.GIT_TERMINAL_PROMPT = '0'
11
+ return own
12
+ }
package/src/html.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { parseFragment } from 'parse5'
2
+ import type { DefaultTreeAdapterMap } from 'parse5'
3
+
4
+ export function escapeHtml(text: string): string {
5
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
6
+ }
7
+
8
+ export interface HtmlAttribute {
9
+ name: string
10
+ value: string
11
+ start: number
12
+ end: number
13
+ }
14
+
15
+ export const URL_ATTRIBUTES: ReadonlySet<string> = new Set(['src', 'poster', 'href'])
16
+
17
+ export function htmlAttributes(html: string, names: ReadonlySet<string>): HtmlAttribute[] {
18
+ const found: HtmlAttribute[] = []
19
+ const visit = (node: DefaultTreeAdapterMap['node']) => {
20
+ if ('attrs' in node) {
21
+ for (const { name, value } of node.attrs) {
22
+ const at = node.sourceCodeLocation?.attrs?.[name]
23
+ if (names.has(name) && at) found.push({ name, value, start: at.startOffset, end: at.endOffset })
24
+ }
25
+ }
26
+ if ('childNodes' in node) node.childNodes.forEach(visit)
27
+ if ('content' in node) visit(node.content)
28
+ }
29
+ visit(parseFragment(html, { sourceCodeLocationInfo: true }))
30
+ return found.sort((a, b) => a.start - b.start)
31
+ }
32
+
33
+ export function mapHtmlUrls(html: string, map: (url: string, attribute: string) => string | undefined): string {
34
+ let out = html
35
+ for (const { name, value, start, end } of htmlAttributes(html, URL_ATTRIBUTES).reverse()) {
36
+ const mapped = map(value, name)
37
+ if (mapped === undefined) continue
38
+ out = `${out.slice(0, start)}${html.slice(start, start + name.length)}="${escapeHtml(mapped)}"${out.slice(end)}`
39
+ }
40
+ return out
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ export * from './contracts.ts'
2
+ export * from './ir.ts'
3
+ export { createAnchorResolverRegistry, parseAnchor } from './anchors.ts'
4
+ export type { ParsedAnchor } from './anchors.ts'
5
+ export { docBlocks, irBlocks, nodeMdast, withProp } from './blocks.ts'
6
+ export { CheckRegistry } from './checks.ts'
7
+ export { errorMessage, isRecord } from './errors.ts'
8
+ export { createEventBus } from './events.ts'
9
+ export { CREDENTIAL_VARIABLE, failedRun, languageId } from './execution.ts'
10
+ export { GIT_SAFE_CONFIG, gitConfigArgs, gitEnvironment } from './git.ts'
11
+ export { escapeHtml, htmlAttributes, mapHtmlUrls, URL_ATTRIBUTES } from './html.ts'
12
+ export type { HtmlAttribute } from './html.ts'
13
+ export { hostName, localRequestRefusal } from './requests.ts'
14
+ export type { RequestHeaders } from './requests.ts'
15
+ export { fingerprint, fnv1a64, normalizeWhitespace } from './fingerprint.ts'
16
+ export { stepKeys, stepTrail, trailStep } from './follow.ts'
17
+ export type { StepKeys } from './follow.ts'
18
+ export { Kernel } from './kernel.ts'
19
+ export type { KernelOptions } from './kernel.ts'
20
+ export { ANCHOR_LOCK_PATH, AnchorLock } from './lock.ts'
21
+ export { prefixed } from './logging.ts'
22
+ export { lowerMyst, roleMark } from './markdown.ts'
23
+ export type { LinkedType, LoweredMarkdown, LowerOptions } from './markdown.ts'
24
+ export { publishedUrl } from './published.ts'
25
+ export type { PublishedUrlOptions } from './published.ts'
26
+ export { blockState, blockText, blockVisible, chapters, isChapterTitle, mdastText, narrationAt, sectionNarration, tokenize } from './reading.ts'
27
+ export type { Chapter } from './reading.ts'
28
+ export { HOST_OWNER, kernelIncompatibility, ServiceRegistry } from './services.ts'
29
+ export type { StartPlan } from './services.ts'