Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions packages/base/frontmatter-field.gts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Diagnostics, ToolContext } from '@cardstack/runtime-common';

import {
Component,
FieldDef,
Expand All @@ -7,6 +9,37 @@ import {
} from './card-api';
import { JsonField } from './json-field';

// Extraction-time inputs handed to `fromFrontmatter` by
// `MarkdownDef.extractAttributes`.
export interface FromFrontmatterContext {
// The markdown file's URL; relative references in the frontmatter (e.g. a
// skill tool's codeRef module) resolve against it.
fileURL: string;
// Owner-carrying context for constructing tool classes during index-time
// schema generation. Only the indexing path provides one; its presence is
// what enables the (module-loading, hence costly) schema work, so
// interactive extract paths never pay for it.
toolContext?: ToolContext;
}

// What `fromFrontmatter` hands back to `MarkdownDef.extractAttributes`.
export interface FromFrontmatterResult {
// The frontmatter field's serialized attributes — the value that lands in
// the search doc and, absent `fileMetaAttributes`, in the file-meta
// resource too.
attributes: Record<string, unknown>;
// Index-only enrichment of `attributes` (e.g. a skill's generated tool
// definitions) destined for the file-meta resource. Kept separate because
// multi-KB generated content must never land in `search_doc`.
fileMetaAttributes?: Record<string, unknown>;
// Diagnostics findings this frontmatter contributes to the indexed row,
// merged onto the row's `diagnostics` by the file indexer and surfaced via
// `/_indexing-errors`. The extract still succeeds — findings never fail the
// row. Which keys a subclass populates is its own knowledge (the base
// contract is just "a bag of Diagnostics").
diagnostics?: Partial<Diagnostics>;
}

// The parsed YAML frontmatter of a markdown file, captured as JSON. The base
// type holds the entire frontmatter in `rawContent`; when the frontmatter
// declares a recognized `boxel.kind` (e.g. `skill`), the concrete instance is a
Expand All @@ -22,12 +55,13 @@ export class FrontmatterField extends FieldDef {

// Map a file's parsed frontmatter into this field's serialized attributes.
// The base keeps the whole frontmatter as the raw copy; subclasses add their
// own typed fields. A subclass is the only thing that knows its own
// frontmatter schema.
static fromFrontmatter(
// own typed fields and any index-time enrichment of them. A subclass is the
// only thing that knows its own frontmatter schema.
static async fromFrontmatter(
frontmatter: Record<string, unknown>,
): Record<string, unknown> {
return { rawContent: frontmatter };
_context?: FromFrontmatterContext,
): Promise<FromFrontmatterResult> {
return { attributes: { rawContent: frontmatter } };
}

static embedded: BaseDefComponent = class Embedded extends Component<
Expand All @@ -39,8 +73,6 @@ export class FrontmatterField extends FieldDef {
| undefined;
return raw?.boxel?.kind ?? '';
}
<template>
{{this.kind}}
</template>
<template>{{this.kind}}</template>
};
}
35 changes: 28 additions & 7 deletions packages/base/markdown-file-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import {
byteStreamToUint8Array,
extractCardReferenceUrls,
extractFileReferenceUrls,
FRONTMATTER_DIAGNOSTICS_SYMBOL,
FRONTMATTER_FILE_META_VALUE_SYMBOL,
FRONTMATTER_PARSE_ERROR_SYMBOL,
identifyCard,
type FrontmatterParseError,
type ToolContext,
} from '@cardstack/runtime-common';
import MarkdownIcon from '@cardstack/boxel-icons/align-box-left-middle';
import {
Expand Down Expand Up @@ -548,7 +551,10 @@ export class MarkdownDef extends FileDef {
static async extractAttributes(
url: string,
getStream: () => Promise<ByteStream>,
options: { contentHash?: string } = {},
// `toolContext` is the owner-carrying context index-time tool schema
// generation constructs tool classes with; only the indexing path
// provides one (see `FromFrontmatterContext`).
options: { contentHash?: string; toolContext?: ToolContext } = {},
): Promise<
SerializedFile<{
title: string;
Expand Down Expand Up @@ -640,17 +646,32 @@ export class MarkdownDef extends FileDef {
}

// `boxel.kind` selects the FrontmatterField subclass; the subclass maps the
// parsed frontmatter into its own field value (the base keeps the raw copy).
// MarkdownDef stays ignorant of any kind's schema. A recognized kind is
// recorded so the field rehydrates as that subclass on read.
// parsed frontmatter into its own field value (the base keeps the raw copy)
// and produces any index-time enrichment of it (e.g. a skill's generated
// tool definitions). MarkdownDef stays ignorant of any kind's schema. A
// recognized kind is recorded so the field rehydrates as that subclass on
// read; the enriched copy and any diagnostics findings ride out-of-band on
// the same symbol channels the parse error uses, so neither leaks into the
// flat `search_doc`.
if (Object.keys(frontmatterData).length > 0) {
let frontmatterFieldClass = frontmatterFieldForKind(kind);
attributes.frontmatter =
frontmatterFieldClass.fromFrontmatter(frontmatterData);
let frontmatterResult = await frontmatterFieldClass.fromFrontmatter(
frontmatterData,
{ fileURL: url, toolContext: options.toolContext },
);
attributes.frontmatter = frontmatterResult.attributes;
let bag = attributes as Record<PropertyKey, unknown>;
if (frontmatterResult.fileMetaAttributes) {
bag[FRONTMATTER_FILE_META_VALUE_SYMBOL] =
frontmatterResult.fileMetaAttributes;
}
if (frontmatterResult.diagnostics) {
bag[FRONTMATTER_DIAGNOSTICS_SYMBOL] = frontmatterResult.diagnostics;
}
if (isKnownFrontmatterKind(kind)) {
let adoptsFrom = identifyCard(frontmatterFieldClass);
if (adoptsFrom) {
(attributes as Record<PropertyKey, unknown>)[fileFieldMetaSymbol] = {
bag[fileFieldMetaSymbol] = {
frontmatter: { adoptsFrom },
};
}
Expand Down
Loading
Loading