(frontmatter: string)
| 355 | } |
| 356 | |
| 357 | function extractGbrainBlock(frontmatter: string): GbrainManifest | null { |
| 358 | // Naive YAML extraction — finds the `gbrain:` key and parses its sub-tree. |
| 359 | // Real YAML parsing avoided to keep zero-deps; gen-skill-docs validates the |
| 360 | // shape strictly at build time. |
| 361 | const lines = frontmatter.split("\n"); |
| 362 | const start = lines.findIndex((l) => /^gbrain\s*:/.test(l)); |
| 363 | if (start === -1) return null; |
| 364 | |
| 365 | // Collect indented lines under `gbrain:` until next top-level key or EOF |
| 366 | const block: string[] = []; |
| 367 | for (let i = start + 1; i < lines.length; i++) { |
| 368 | const line = lines[i]; |
| 369 | if (/^[A-Za-z_][A-Za-z0-9_-]*\s*:/.test(line)) break; // next top-level key |
| 370 | block.push(line); |
| 371 | } |
| 372 | |
| 373 | const text = block.join("\n"); |
| 374 | // Extract schema number |
| 375 | const schemaMatch = text.match(/\n\s*schema\s*:\s*(\d+)/); |
| 376 | const schema = schemaMatch ? parseInt(schemaMatch[1], 10) : 1; |
| 377 | |
| 378 | // Extract context_queries items |
| 379 | const queries: GbrainManifestQuery[] = []; |
| 380 | const cqMatch = text.match(/\n\s*context_queries\s*:\s*\n([\s\S]+)/); |
| 381 | if (cqMatch) { |
| 382 | const cqText = cqMatch[1]; |
| 383 | // Split using a positive lookahead so each chunk begins with the list-item dash. |
| 384 | // Pattern: line starting with 4-6 spaces + "-" + whitespace. |
| 385 | const rawItems = cqText.split(/(?=^[ ]{4,6}-\s)/m); |
| 386 | const items = rawItems.filter((s) => /^[ ]{4,6}-\s/.test(s)); |
| 387 | for (const item of items) { |
| 388 | const q: Partial<GbrainManifestQuery> = {}; |
| 389 | // Strip the leading list-item marker so id/kind/etc. regexes can use line-start. |
| 390 | const body = item.replace(/^[ ]{4,6}-\s+/, " "); |
| 391 | const idM = body.match(/(?:^|\n)\s*id\s*:\s*([^\n]+)/); |
| 392 | const kindM = body.match(/(?:^|\n)\s*kind\s*:\s*([^\n]+)/); |
| 393 | const renderM = body.match(/(?:^|\n)\s*render_as\s*:\s*"?([^"\n]+?)"?\s*$/m); |
| 394 | const queryM = body.match(/(?:^|\n)\s*query\s*:\s*"?([^"\n]+?)"?\s*$/m); |
| 395 | const limitM = body.match(/(?:^|\n)\s*limit\s*:\s*(\d+)/); |
| 396 | const globM = body.match(/(?:^|\n)\s*glob\s*:\s*"?([^"\n]+?)"?\s*$/m); |
| 397 | const sortM = body.match(/(?:^|\n)\s*sort\s*:\s*([^\n]+)/); |
| 398 | const tailM = body.match(/(?:^|\n)\s*tail\s*:\s*(\d+)/); |
| 399 | |
| 400 | if (idM) q.id = idM[1].trim(); |
| 401 | if (kindM) { |
| 402 | const k = kindM[1].trim(); |
| 403 | if (k === "vector" || k === "list" || k === "filesystem") q.kind = k; |
| 404 | } |
| 405 | if (renderM) q.render_as = renderM[1].trim(); |
| 406 | if (queryM) q.query = queryM[1].trim(); |
| 407 | if (limitM) q.limit = parseInt(limitM[1], 10); |
| 408 | if (globM) q.glob = globM[1].trim(); |
| 409 | if (sortM) q.sort = sortM[1].trim(); |
| 410 | if (tailM) q.tail = parseInt(tailM[1], 10); |
| 411 | |
| 412 | if (q.id && q.kind && q.render_as) { |
| 413 | queries.push(q as GbrainManifestQuery); |
| 414 | } |
no test coverage detected