@erclx/aitk 0.100.0 → 0.101.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-orchestrate/SKILL.md +2 -2
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +4 -4
- package/claude/skills/claude-orchestrate/scripts/poll.sh +11 -10
- package/claude/skills/claude-pr-review/SKILL.md +16 -16
- package/claude/skills/claude-teach/REQUIREMENT.md +2 -1
- package/claude/skills/claude-teach/SKILL.md +40 -9
- package/docs/agents/commands.md +5 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/teach.md +119 -0
- package/docs/ai-workflow.md +1 -1
- package/docs/operating-model.md +9 -8
- package/package.json +1 -1
- package/src/cli.ts +4 -0
- package/src/commands/teach.ts +650 -0
- package/src/records/validate.ts +8 -7
- package/src/teach/workspace.ts +797 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { relative } from 'node:path'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import {
|
|
4
|
+
defineTerms,
|
|
5
|
+
type ListOutcome,
|
|
6
|
+
listWorkspaces,
|
|
7
|
+
type OpenOutcome,
|
|
8
|
+
openWorkspace,
|
|
9
|
+
type ReadOutcome,
|
|
10
|
+
readWorkspace,
|
|
11
|
+
recordSources,
|
|
12
|
+
type Source,
|
|
13
|
+
type SourceOutcome,
|
|
14
|
+
type TeachRefused,
|
|
15
|
+
type Term,
|
|
16
|
+
type TermOutcome,
|
|
17
|
+
type WorkspaceSummary,
|
|
18
|
+
} from '@/teach/workspace'
|
|
19
|
+
import {
|
|
20
|
+
intro,
|
|
21
|
+
logAdd,
|
|
22
|
+
logError,
|
|
23
|
+
logInfo,
|
|
24
|
+
logStep,
|
|
25
|
+
logWarn,
|
|
26
|
+
outro,
|
|
27
|
+
pipeOutput,
|
|
28
|
+
} from '@/ui'
|
|
29
|
+
import { mainWorktreeRoot } from '@/worktree'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A pair as the command line spells it, split on the first `=` so a value
|
|
33
|
+
* carrying its own separator survives. A source URL is the case that needs it.
|
|
34
|
+
*/
|
|
35
|
+
const PAIR = /^([^=]+)=([\s\S]+)$/
|
|
36
|
+
|
|
37
|
+
interface ListCommandOptions {
|
|
38
|
+
readonly json?: boolean
|
|
39
|
+
readonly root?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface OpenCommandOptions {
|
|
43
|
+
readonly date?: string
|
|
44
|
+
readonly json?: boolean
|
|
45
|
+
readonly outOfScope?: readonly string[]
|
|
46
|
+
readonly root?: string
|
|
47
|
+
readonly startingPoint?: string
|
|
48
|
+
readonly subject?: string
|
|
49
|
+
readonly success?: readonly string[]
|
|
50
|
+
readonly title?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ResourceCommandOptions {
|
|
54
|
+
readonly json?: boolean
|
|
55
|
+
readonly lead?: readonly string[]
|
|
56
|
+
readonly read?: readonly string[]
|
|
57
|
+
readonly root?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface GlossaryCommandOptions {
|
|
61
|
+
readonly firstSeen?: string
|
|
62
|
+
readonly json?: boolean
|
|
63
|
+
readonly root?: string
|
|
64
|
+
readonly term?: readonly string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function register(program: Command): void {
|
|
68
|
+
const teach = program
|
|
69
|
+
.command('teach')
|
|
70
|
+
.description('Manage learning workspaces in .claude/teach/')
|
|
71
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
72
|
+
|
|
73
|
+
teach
|
|
74
|
+
.command('list')
|
|
75
|
+
.description('List learning workspaces, or what one workspace holds')
|
|
76
|
+
.argument('[topic]', 'Workspace folder or topic, as in regular-expressions')
|
|
77
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
78
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
79
|
+
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
80
|
+
.addHelpText(
|
|
81
|
+
'after',
|
|
82
|
+
[
|
|
83
|
+
'',
|
|
84
|
+
'Exit codes:',
|
|
85
|
+
' 0 the workspaces or their contents were listed',
|
|
86
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
87
|
+
'',
|
|
88
|
+
'With no topic it reports one line per workspace and the ordinal an',
|
|
89
|
+
'open would take. With one it reports the files behind each count.',
|
|
90
|
+
'',
|
|
91
|
+
'A workspace not named NN-<topic> is still listed. It sorts last and',
|
|
92
|
+
'moves no ordinal, since dropping it hides the folder needing a fix.',
|
|
93
|
+
'',
|
|
94
|
+
'Examples:',
|
|
95
|
+
' aitk teach list',
|
|
96
|
+
' aitk teach list regular-expressions --json',
|
|
97
|
+
'',
|
|
98
|
+
].join('\n'),
|
|
99
|
+
)
|
|
100
|
+
.action(async (topic: string | undefined, opts: ListCommandOptions) => {
|
|
101
|
+
process.exitCode = await runList(topic, opts)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
teach
|
|
105
|
+
.command('open')
|
|
106
|
+
.description('Open a workspace at the next ordinal with its required files')
|
|
107
|
+
.argument('<topic>', 'Kebab-case topic, as in regular-expressions')
|
|
108
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
109
|
+
.option('--subject <line>', 'One line stating what the workspace covers')
|
|
110
|
+
.option('--starting-point <text>', 'What the learner already knows')
|
|
111
|
+
.option(
|
|
112
|
+
'--success <line>',
|
|
113
|
+
'Observable thing the learner will be able to do, repeatable',
|
|
114
|
+
collect,
|
|
115
|
+
[] as string[],
|
|
116
|
+
)
|
|
117
|
+
.option(
|
|
118
|
+
'--out-of-scope <line>',
|
|
119
|
+
'What this workspace does not cover, repeatable',
|
|
120
|
+
collect,
|
|
121
|
+
[] as string[],
|
|
122
|
+
)
|
|
123
|
+
.option('--title <text>', 'Title, defaulting to the topic in sentence case')
|
|
124
|
+
.option('--date <YYYY-MM-DD>', 'Opening date, defaulting to today')
|
|
125
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
126
|
+
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
127
|
+
.addHelpText(
|
|
128
|
+
'after',
|
|
129
|
+
[
|
|
130
|
+
'',
|
|
131
|
+
'Exit codes:',
|
|
132
|
+
' 0 the workspace was created',
|
|
133
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
134
|
+
'',
|
|
135
|
+
'It derives the ordinal from the highest already present and writes',
|
|
136
|
+
'MISSION.md, RESOURCES.md, and GLOSSARY.md. A topic another workspace',
|
|
137
|
+
'already covers is refused, since a second one forks the records.',
|
|
138
|
+
'',
|
|
139
|
+
'Examples:',
|
|
140
|
+
' aitk teach open regular-expressions --subject "Reading and writing regular expressions" \\',
|
|
141
|
+
' --starting-point "Comfortable with the shell, has never written a group" \\',
|
|
142
|
+
' --success "Write a pattern matching a date" --success "Explain a backreference"',
|
|
143
|
+
'',
|
|
144
|
+
].join('\n'),
|
|
145
|
+
)
|
|
146
|
+
.action(async (topic: string, opts: OpenCommandOptions) => {
|
|
147
|
+
process.exitCode = await runOpen(topic, opts)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
teach
|
|
151
|
+
.command('resource')
|
|
152
|
+
.description('Record sources and leads in a workspace RESOURCES.md')
|
|
153
|
+
.argument('<topic>', 'Workspace folder or topic, as in regular-expressions')
|
|
154
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
155
|
+
.option(
|
|
156
|
+
'--read <title=url>',
|
|
157
|
+
'Source that stands behind the material, repeatable',
|
|
158
|
+
collect,
|
|
159
|
+
[] as string[],
|
|
160
|
+
)
|
|
161
|
+
.option(
|
|
162
|
+
'--lead <title=url>',
|
|
163
|
+
'Source found and not opened, repeatable',
|
|
164
|
+
collect,
|
|
165
|
+
[] as string[],
|
|
166
|
+
)
|
|
167
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
168
|
+
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
169
|
+
.addHelpText(
|
|
170
|
+
'after',
|
|
171
|
+
[
|
|
172
|
+
'',
|
|
173
|
+
'Exit codes:',
|
|
174
|
+
' 0 every source was written',
|
|
175
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
176
|
+
'',
|
|
177
|
+
'The pair splits on the first =, so a URL carrying one survives. Say',
|
|
178
|
+
'in the title which claims rest on the source, since the entry is the',
|
|
179
|
+
'only place a later session reads that from.',
|
|
180
|
+
'',
|
|
181
|
+
'A URL already listed under either heading is refused rather than',
|
|
182
|
+
'repeated, because two entries split what rests on one source.',
|
|
183
|
+
'',
|
|
184
|
+
'Examples:',
|
|
185
|
+
' aitk teach resource regular-expressions --read "MDN regular expressions=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions"',
|
|
186
|
+
' aitk teach resource regular-expressions --lead "RE2 syntax=https://github.com/google/re2/wiki/Syntax" --json',
|
|
187
|
+
'',
|
|
188
|
+
].join('\n'),
|
|
189
|
+
)
|
|
190
|
+
.action(async (topic: string, opts: ResourceCommandOptions) => {
|
|
191
|
+
process.exitCode = await runResource(topic, opts)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
teach
|
|
195
|
+
.command('glossary')
|
|
196
|
+
.description('Add terms to a workspace GLOSSARY.md, alphabetically')
|
|
197
|
+
.argument('<topic>', 'Workspace folder or topic, as in regular-expressions')
|
|
198
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
199
|
+
.option(
|
|
200
|
+
'--term <term=definition>',
|
|
201
|
+
'Term the subject defines, repeatable',
|
|
202
|
+
collect,
|
|
203
|
+
[] as string[],
|
|
204
|
+
)
|
|
205
|
+
.option(
|
|
206
|
+
'--first-seen <file>',
|
|
207
|
+
'Lesson or reference page the batch first defines these in',
|
|
208
|
+
)
|
|
209
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
210
|
+
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
211
|
+
.addHelpText(
|
|
212
|
+
'after',
|
|
213
|
+
[
|
|
214
|
+
'',
|
|
215
|
+
'Exit codes:',
|
|
216
|
+
' 0 every term now carries an entry',
|
|
217
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
218
|
+
'',
|
|
219
|
+
'One call writes one file, which is what keeps a batch from racing on',
|
|
220
|
+
'the glossary every term shares. --first-seen names one page for the',
|
|
221
|
+
'whole batch, since a batch comes from one lesson.',
|
|
222
|
+
'',
|
|
223
|
+
'A term already defined is refused rather than replaced. A definition',
|
|
224
|
+
'the subject has moved under is a revision of the entry it has.',
|
|
225
|
+
'',
|
|
226
|
+
'Examples:',
|
|
227
|
+
' aitk teach glossary regular-expressions --term "capture group=A parenthesised part of a pattern whose match is kept" --first-seen 0002-groups.html',
|
|
228
|
+
'',
|
|
229
|
+
].join('\n'),
|
|
230
|
+
)
|
|
231
|
+
.action(async (topic: string, opts: GlossaryCommandOptions) => {
|
|
232
|
+
process.exitCode = await runGlossary(topic, opts)
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function collect(value: string, previous: string[]): string[] {
|
|
237
|
+
return [...previous, value]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Splits every pair or reports the ones that carry no separator. Both halves
|
|
242
|
+
* are reported together, so a caller passing four pairs learns about all the
|
|
243
|
+
* broken ones from one run rather than from four.
|
|
244
|
+
*/
|
|
245
|
+
function parsePairs(
|
|
246
|
+
raw: readonly string[],
|
|
247
|
+
): { pairs: Array<[string, string]> } | { invalid: string[] } {
|
|
248
|
+
const pairs: Array<[string, string]> = []
|
|
249
|
+
const invalid: string[] = []
|
|
250
|
+
|
|
251
|
+
for (const entry of raw) {
|
|
252
|
+
const match = PAIR.exec(entry)
|
|
253
|
+
const left = match?.[1].trim()
|
|
254
|
+
const right = match?.[2].trim()
|
|
255
|
+
|
|
256
|
+
if (!left || !right) {
|
|
257
|
+
invalid.push(entry)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
pairs.push([left, right])
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return invalid.length > 0 ? { invalid } : { pairs }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function badInput(
|
|
268
|
+
message: string,
|
|
269
|
+
detail: readonly string[] = [],
|
|
270
|
+
): TeachRefused {
|
|
271
|
+
return { ok: false, reason: 'bad-input', message, detail }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function rootFor(given: string | undefined): Promise<string> {
|
|
275
|
+
return given ?? (await mainWorktreeRoot())
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function runList(
|
|
279
|
+
topic: string | undefined,
|
|
280
|
+
opts: ListCommandOptions,
|
|
281
|
+
): Promise<number> {
|
|
282
|
+
const emitJson = opts.json ?? false
|
|
283
|
+
const root = await rootFor(opts.root)
|
|
284
|
+
|
|
285
|
+
if (topic === undefined) {
|
|
286
|
+
return reportList(await listWorkspaces(root), emitJson, root)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return reportWorkspace(await readWorkspace(root, topic), emitJson, root)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function runOpen(
|
|
293
|
+
topic: string,
|
|
294
|
+
opts: OpenCommandOptions,
|
|
295
|
+
): Promise<number> {
|
|
296
|
+
const emitJson = opts.json ?? false
|
|
297
|
+
|
|
298
|
+
if (!opts.subject) {
|
|
299
|
+
return reportRefusal(
|
|
300
|
+
'aitk teach open',
|
|
301
|
+
badInput('No subject. Pass --subject <line>.'),
|
|
302
|
+
emitJson,
|
|
303
|
+
process.cwd(),
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!opts.startingPoint) {
|
|
308
|
+
return reportRefusal(
|
|
309
|
+
'aitk teach open',
|
|
310
|
+
badInput(
|
|
311
|
+
'No starting point. Pass --starting-point <text>, so difficulty has a floor.',
|
|
312
|
+
),
|
|
313
|
+
emitJson,
|
|
314
|
+
process.cwd(),
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const root = await rootFor(opts.root)
|
|
319
|
+
|
|
320
|
+
return reportOpen(
|
|
321
|
+
await openWorkspace(root, {
|
|
322
|
+
topic,
|
|
323
|
+
subject: opts.subject,
|
|
324
|
+
startingPoint: opts.startingPoint,
|
|
325
|
+
success: opts.success ?? [],
|
|
326
|
+
outOfScope: opts.outOfScope ?? [],
|
|
327
|
+
title: opts.title,
|
|
328
|
+
date: opts.date,
|
|
329
|
+
}),
|
|
330
|
+
emitJson,
|
|
331
|
+
root,
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function runResource(
|
|
336
|
+
topic: string,
|
|
337
|
+
opts: ResourceCommandOptions,
|
|
338
|
+
): Promise<number> {
|
|
339
|
+
const emitJson = opts.json ?? false
|
|
340
|
+
const raw = [...(opts.read ?? []), ...(opts.lead ?? [])]
|
|
341
|
+
|
|
342
|
+
if (raw.length === 0) {
|
|
343
|
+
return reportRefusal(
|
|
344
|
+
'aitk teach resource',
|
|
345
|
+
badInput('No source given. Pass --read or --lead as <title>=<url>.'),
|
|
346
|
+
emitJson,
|
|
347
|
+
process.cwd(),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const parsed = parsePairs(raw)
|
|
352
|
+
|
|
353
|
+
if ('invalid' in parsed) {
|
|
354
|
+
return reportRefusal(
|
|
355
|
+
'aitk teach resource',
|
|
356
|
+
badInput(
|
|
357
|
+
`Not a title and url: ${parsed.invalid.join(', ')}`,
|
|
358
|
+
parsed.invalid,
|
|
359
|
+
),
|
|
360
|
+
emitJson,
|
|
361
|
+
process.cwd(),
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const readCount = (opts.read ?? []).length
|
|
366
|
+
const sources: Source[] = parsed.pairs.map(([title, url]) => ({ title, url }))
|
|
367
|
+
const root = await rootFor(opts.root)
|
|
368
|
+
|
|
369
|
+
return reportResource(
|
|
370
|
+
await recordSources(
|
|
371
|
+
root,
|
|
372
|
+
topic,
|
|
373
|
+
sources.slice(0, readCount),
|
|
374
|
+
sources.slice(readCount),
|
|
375
|
+
),
|
|
376
|
+
emitJson,
|
|
377
|
+
root,
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function runGlossary(
|
|
382
|
+
topic: string,
|
|
383
|
+
opts: GlossaryCommandOptions,
|
|
384
|
+
): Promise<number> {
|
|
385
|
+
const emitJson = opts.json ?? false
|
|
386
|
+
const raw = opts.term ?? []
|
|
387
|
+
|
|
388
|
+
if (raw.length === 0) {
|
|
389
|
+
return reportRefusal(
|
|
390
|
+
'aitk teach glossary',
|
|
391
|
+
badInput('No term given. Pass --term <term>=<definition>.'),
|
|
392
|
+
emitJson,
|
|
393
|
+
process.cwd(),
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const parsed = parsePairs(raw)
|
|
398
|
+
|
|
399
|
+
if ('invalid' in parsed) {
|
|
400
|
+
return reportRefusal(
|
|
401
|
+
'aitk teach glossary',
|
|
402
|
+
badInput(
|
|
403
|
+
`Not a term and definition: ${parsed.invalid.join(', ')}`,
|
|
404
|
+
parsed.invalid,
|
|
405
|
+
),
|
|
406
|
+
emitJson,
|
|
407
|
+
process.cwd(),
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const terms: Term[] = parsed.pairs.map(([term, definition]) => ({
|
|
412
|
+
term,
|
|
413
|
+
definition,
|
|
414
|
+
}))
|
|
415
|
+
|
|
416
|
+
const duplicated = terms
|
|
417
|
+
.map((term) => term.term.toLowerCase())
|
|
418
|
+
.filter((term, index, all) => all.indexOf(term) !== index)
|
|
419
|
+
|
|
420
|
+
if (duplicated.length > 0) {
|
|
421
|
+
return reportRefusal(
|
|
422
|
+
'aitk teach glossary',
|
|
423
|
+
badInput(`Two definitions for ${[...new Set(duplicated)].join(', ')}.`),
|
|
424
|
+
emitJson,
|
|
425
|
+
process.cwd(),
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const root = await rootFor(opts.root)
|
|
430
|
+
|
|
431
|
+
return reportGlossary(
|
|
432
|
+
await defineTerms(root, topic, terms, opts.firstSeen),
|
|
433
|
+
emitJson,
|
|
434
|
+
root,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function reportRefusal(
|
|
439
|
+
title: string,
|
|
440
|
+
refused: TeachRefused,
|
|
441
|
+
emitJson: boolean,
|
|
442
|
+
root: string,
|
|
443
|
+
): number {
|
|
444
|
+
// The framed branch reaches stderr through logError, so the bare write is
|
|
445
|
+
// what keeps the JSON mode from reporting the reason on stdout alone.
|
|
446
|
+
if (emitJson) {
|
|
447
|
+
process.stderr.write(`${refused.message}\n`)
|
|
448
|
+
process.stdout.write(
|
|
449
|
+
`${JSON.stringify({
|
|
450
|
+
ok: false,
|
|
451
|
+
root,
|
|
452
|
+
reason: refused.reason,
|
|
453
|
+
message: refused.message,
|
|
454
|
+
detail: refused.detail,
|
|
455
|
+
})}\n`,
|
|
456
|
+
)
|
|
457
|
+
return 1
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
intro(title)
|
|
461
|
+
logStep('Refused')
|
|
462
|
+
logError(refused.message)
|
|
463
|
+
if (refused.detail.length > 0) pipeOutput(refused.detail.join('\n'))
|
|
464
|
+
outro()
|
|
465
|
+
|
|
466
|
+
return 1
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function describe(workspace: WorkspaceSummary): string {
|
|
470
|
+
return `${workspace.slug}: ${workspace.lessons} lesson(s), ${workspace.records} record(s), ${workspace.reference} reference page(s), ${workspace.terms} term(s)`
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function reportList(
|
|
474
|
+
outcome: ListOutcome,
|
|
475
|
+
emitJson: boolean,
|
|
476
|
+
root: string,
|
|
477
|
+
): number {
|
|
478
|
+
if (!outcome.ok)
|
|
479
|
+
return reportRefusal('aitk teach list', outcome, emitJson, root)
|
|
480
|
+
|
|
481
|
+
if (emitJson) {
|
|
482
|
+
process.stdout.write(
|
|
483
|
+
`${JSON.stringify({
|
|
484
|
+
ok: true,
|
|
485
|
+
root,
|
|
486
|
+
workspaces: outcome.workspaces,
|
|
487
|
+
next: outcome.next,
|
|
488
|
+
})}\n`,
|
|
489
|
+
)
|
|
490
|
+
return 0
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
intro('aitk teach list')
|
|
494
|
+
logStep(outcome.workspaces.length > 0 ? 'Workspaces' : 'No workspaces')
|
|
495
|
+
|
|
496
|
+
for (const workspace of outcome.workspaces) logInfo(describe(workspace))
|
|
497
|
+
|
|
498
|
+
const incomplete = outcome.workspaces.filter(
|
|
499
|
+
(workspace) => workspace.missing.length > 0,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if (incomplete.length > 0) {
|
|
503
|
+
logStep('Missing a required file')
|
|
504
|
+
for (const workspace of incomplete) {
|
|
505
|
+
logWarn(`${workspace.slug}: no ${workspace.missing.join(' and no ')}`)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
logStep('Next ordinal')
|
|
510
|
+
logInfo(outcome.next)
|
|
511
|
+
outro()
|
|
512
|
+
|
|
513
|
+
return 0
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function reportWorkspace(
|
|
517
|
+
outcome: ReadOutcome,
|
|
518
|
+
emitJson: boolean,
|
|
519
|
+
root: string,
|
|
520
|
+
): number {
|
|
521
|
+
if (!outcome.ok)
|
|
522
|
+
return reportRefusal('aitk teach list', outcome, emitJson, root)
|
|
523
|
+
|
|
524
|
+
const workspace = outcome.workspace
|
|
525
|
+
|
|
526
|
+
if (emitJson) {
|
|
527
|
+
process.stdout.write(`${JSON.stringify({ ok: true, root, workspace })}\n`)
|
|
528
|
+
return 0
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
intro('aitk teach list')
|
|
532
|
+
logStep(workspace.slug)
|
|
533
|
+
logInfo(`${workspace.title ?? 'no title'}, opened ${workspace.opened ?? '?'}`)
|
|
534
|
+
logInfo(workspace.path)
|
|
535
|
+
|
|
536
|
+
for (const [label, files] of [
|
|
537
|
+
['Lessons', workspace.lessonFiles],
|
|
538
|
+
['Learning records', workspace.recordFiles],
|
|
539
|
+
['Reference', workspace.referenceFiles],
|
|
540
|
+
] as const) {
|
|
541
|
+
logStep(files.length > 0 ? label : `${label} (none)`)
|
|
542
|
+
for (const file of files) logInfo(file)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
logStep(workspace.glossary.length > 0 ? 'Glossary' : 'Glossary (empty)')
|
|
546
|
+
for (const entry of workspace.glossary) logInfo(entry)
|
|
547
|
+
|
|
548
|
+
if (workspace.missing.length > 0) {
|
|
549
|
+
logStep('Missing a required file')
|
|
550
|
+
logWarn(`no ${workspace.missing.join(' and no ')}`)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
outro()
|
|
554
|
+
|
|
555
|
+
return 0
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function reportOpen(
|
|
559
|
+
outcome: OpenOutcome,
|
|
560
|
+
emitJson: boolean,
|
|
561
|
+
root: string,
|
|
562
|
+
): number {
|
|
563
|
+
if (!outcome.ok)
|
|
564
|
+
return reportRefusal('aitk teach open', outcome, emitJson, root)
|
|
565
|
+
|
|
566
|
+
if (emitJson) {
|
|
567
|
+
process.stdout.write(
|
|
568
|
+
`${JSON.stringify({
|
|
569
|
+
ok: true,
|
|
570
|
+
root,
|
|
571
|
+
slug: outcome.slug,
|
|
572
|
+
path: outcome.path,
|
|
573
|
+
created: outcome.created,
|
|
574
|
+
})}\n`,
|
|
575
|
+
)
|
|
576
|
+
return 0
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
intro('aitk teach open')
|
|
580
|
+
logStep('Opened')
|
|
581
|
+
logInfo(outcome.slug)
|
|
582
|
+
for (const file of outcome.created) logAdd(file)
|
|
583
|
+
outro()
|
|
584
|
+
|
|
585
|
+
return 0
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function reportResource(
|
|
589
|
+
outcome: SourceOutcome,
|
|
590
|
+
emitJson: boolean,
|
|
591
|
+
root: string,
|
|
592
|
+
): number {
|
|
593
|
+
if (!outcome.ok) {
|
|
594
|
+
return reportRefusal('aitk teach resource', outcome, emitJson, root)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (emitJson) {
|
|
598
|
+
process.stdout.write(
|
|
599
|
+
`${JSON.stringify({
|
|
600
|
+
ok: true,
|
|
601
|
+
root,
|
|
602
|
+
slug: outcome.slug,
|
|
603
|
+
path: relative(root, outcome.path),
|
|
604
|
+
read: outcome.read,
|
|
605
|
+
leads: outcome.leads,
|
|
606
|
+
})}\n`,
|
|
607
|
+
)
|
|
608
|
+
return 0
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
intro('aitk teach resource')
|
|
612
|
+
logStep('Recorded')
|
|
613
|
+
for (const source of outcome.read) logInfo(`read: ${source.title}`)
|
|
614
|
+
for (const source of outcome.leads) logInfo(`lead: ${source.title}`)
|
|
615
|
+
logAdd(relative(root, outcome.path))
|
|
616
|
+
outro()
|
|
617
|
+
|
|
618
|
+
return 0
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function reportGlossary(
|
|
622
|
+
outcome: TermOutcome,
|
|
623
|
+
emitJson: boolean,
|
|
624
|
+
root: string,
|
|
625
|
+
): number {
|
|
626
|
+
if (!outcome.ok) {
|
|
627
|
+
return reportRefusal('aitk teach glossary', outcome, emitJson, root)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (emitJson) {
|
|
631
|
+
process.stdout.write(
|
|
632
|
+
`${JSON.stringify({
|
|
633
|
+
ok: true,
|
|
634
|
+
root,
|
|
635
|
+
slug: outcome.slug,
|
|
636
|
+
path: relative(root, outcome.path),
|
|
637
|
+
defined: outcome.defined,
|
|
638
|
+
})}\n`,
|
|
639
|
+
)
|
|
640
|
+
return 0
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
intro('aitk teach glossary')
|
|
644
|
+
logStep('Defined')
|
|
645
|
+
for (const term of outcome.defined) logInfo(term.term)
|
|
646
|
+
logAdd(relative(root, outcome.path))
|
|
647
|
+
outro()
|
|
648
|
+
|
|
649
|
+
return 0
|
|
650
|
+
}
|
package/src/records/validate.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { readdir, readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
5
5
|
import { linesOutsideFences } from '@/markdown/scan'
|
|
6
|
+
import {
|
|
7
|
+
TEACH_GLOSSARY,
|
|
8
|
+
TEACH_MISSION,
|
|
9
|
+
TEACH_RECORDS,
|
|
10
|
+
TEACH_REFERENCE,
|
|
11
|
+
TEACH_RESOURCES,
|
|
12
|
+
WORKSPACE_NAME,
|
|
13
|
+
} from '@/teach/workspace'
|
|
6
14
|
|
|
7
15
|
export const RECORD_KINDS = [
|
|
8
16
|
'plans',
|
|
@@ -568,14 +576,7 @@ async function checkDump(dir: string, slug: string): Promise<Finding[]> {
|
|
|
568
576
|
return [...findings, ...perCluster.flat()]
|
|
569
577
|
}
|
|
570
578
|
|
|
571
|
-
const TEACH_MISSION = 'MISSION.md'
|
|
572
|
-
const TEACH_RESOURCES = 'RESOURCES.md'
|
|
573
|
-
const TEACH_GLOSSARY = 'GLOSSARY.md'
|
|
574
|
-
const TEACH_REFERENCE = 'reference'
|
|
575
|
-
const TEACH_RECORDS = 'learning-records'
|
|
576
|
-
|
|
577
579
|
const TEACH_SUCCESS = /^##[ \t]+Success looks like[ \t]*$/
|
|
578
|
-
const WORKSPACE_NAME = /^\d{2}-[a-z0-9]+(-[a-z0-9]+)*$/
|
|
579
580
|
const NUMBERED_RECORD = /^\d{4}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
|
|
580
581
|
/**
|
|
581
582
|
* A kebab slug that does not open with an ordinal. The lookahead rejects a
|