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
5 changes: 5 additions & 0 deletions .changeset/skill-version-stamp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tailor-platform/app-shell": patch
---

The bundled `app-shell-patterns` skill now records the `@tailor-platform/app-shell` version it was generated from, in its `metadata.app-shell-version` frontmatter and in a new "Version" section. The skill tells the agent to compare it with the installed package and to suggest re-syncing a vendored copy that has fallen behind.
3 changes: 2 additions & 1 deletion docs-kit.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"outDir": "packages/core/skills/app-shell-patterns",
"templatePath": "docs-src/skill/SKILL.template.md",
"migrationsSource": "docs/migrations.md",
"repoBlobUrl": "https://github.com/tailor-platform/app-shell/blob/main"
"repoBlobUrl": "https://github.com/tailor-platform/app-shell/blob/main",
"versionSource": "packages/core/package.json"
}
},
"policy": {
Expand Down
2 changes: 1 addition & 1 deletion docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,6 @@
"packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md": "59c93617dc135033",
"packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "5a6612349cb50ef1",
"packages/core/skills/app-shell-patterns/references/migrations.md": "e0286979718e2769",
"packages/core/skills/app-shell-patterns/SKILL.md": "1aa14970eee9bce0"
"packages/core/skills/app-shell-patterns/SKILL.md": "de250e21921fc3e8"
}
}
6 changes: 6 additions & 0 deletions docs-src/skill/SKILL.template.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
name: app-shell-patterns
description: "Best-practice UI patterns and correct component usage for building pages in apps that use @tailor-platform/app-shell. Use when: building or editing any screen, page, list, table, detail view, form, modal, dialog, wizard, or bulk/confirm/toast interaction in an app with @tailor-platform/app-shell installed — when choosing the right AppShell component, layout, or design token for a UI — or when upgrading @tailor-platform/app-shell, or diagnosing styling, theming, or dark-mode breakage that appeared after a version bump."
metadata:
app-shell-version: "{{APP_SHELL_VERSION}}"
---

# App-Shell Patterns
Expand All @@ -9,6 +11,10 @@ description: "Best-practice UI patterns and correct component usage for building

Select and implement the correct UI pattern using @tailor-platform/app-shell components.

## Version

This skill was generated from `@tailor-platform/app-shell` **{{APP_SHELL_VERSION}}**. Before relying on it, compare that with the version installed in the app you're working on (resolve `@tailor-platform/app-shell/package.json` from that app, not the monorepo root). If they differ, this copy is stale: tell the user and suggest re-syncing it from the installed package's `skills/app-shell-patterns/` directory.

## Concepts

Cross-cutting guides that underpin every screen — styling & theming and the token system, GraphQL data access, building custom components, plus auth and routing. Patterns and components build on these.
Expand Down
9 changes: 8 additions & 1 deletion packages/docs-kit/src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { hash, normalizeText } from "./hash";
import { readManifest } from "./manifest";
import { discoverOutlines } from "./outline";
import { loadSurface, snapshotHashForSlug } from "./project";
import { computeSkill } from "./skill";
import { computeSkill, computeStampedSkill } from "./skill";
import { unmanagedOutputs, walkMarkdown } from "./tree";

export interface CheckResult {
Expand Down Expand Up @@ -140,6 +140,13 @@ export function check(repoRoot: string): CheckResult {
});
}
}
// The manifest hashes unstamped content, so validate the stamp separately —
// otherwise a broken version source would only surface at publish time.
try {
computeStampedSkill(repoRoot, config, outlines);
} catch (e) {
findings.push({ level: "block", slug: "skill", message: (e as Error).message });
}
}

// Nothing unmanaged may live in the generated tree. Every .md under an output
Expand Down
52 changes: 52 additions & 0 deletions packages/docs-kit/src/skill.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { describe, expect, it } from "vitest";

import { readSkillVersion, stampSkill } from "./skill";
import type { DocsConfig } from "./types";

const configWith = (versionSource?: string): DocsConfig => ({
roots: [],
tsconfig: "",
indexFile: "",
manifestFile: "",
categories: [],
enforceCoverage: false,
exclusions: [],
skill: { outDir: "", templatePath: "", migrationsSource: "", repoBlobUrl: "", versionSource },
});

describe("stampSkill", () => {
it("replaces every version placeholder", () => {
const content = 'app-shell-version: "{{APP_SHELL_VERSION}}"\n**{{APP_SHELL_VERSION}}**';
expect(stampSkill(content, "1.17.0")).toBe('app-shell-version: "1.17.0"\n**1.17.0**');
});

it("leaves placeholder-free content untouched without a version", () => {
expect(stampSkill("no stamp here", null)).toBe("no stamp here");
});

it("throws rather than ship an unfilled placeholder", () => {
expect(() => stampSkill("{{APP_SHELL_VERSION}}", null)).toThrow(/versionSource/);
});
});

describe("readSkillVersion", () => {
it("reads the version from the configured package.json", () => {
const root = mkdtempSync(join(tmpdir(), "docs-kit-"));
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "2.0.0" }));
expect(readSkillVersion(root, configWith("package.json"))).toBe("2.0.0");
});

it("returns null when no source is configured", () => {
expect(readSkillVersion("/nonexistent", configWith())).toBeNull();
});

it("throws when the package.json has no version", () => {
const root = mkdtempSync(join(tmpdir(), "docs-kit-"));
writeFileSync(join(root, "package.json"), "{}");
expect(() => readSkillVersion(root, configWith("package.json"))).toThrow(/no "version"/);
});
});
39 changes: 39 additions & 0 deletions packages/docs-kit/src/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,45 @@ function entryTable(list: Outline[], dir: string): string {
return blocks.join("\n\n");
}

const VERSION_PLACEHOLDER = /\{\{APP_SHELL_VERSION\}\}/g;

/** Read the version stamped into the skill, or null when no source is configured. */
export function readSkillVersion(repoRoot: string, config: DocsConfig): string | null {
const source = config.skill?.versionSource;
if (!source) return null;
const { version } = JSON.parse(readFileSync(join(repoRoot, source), "utf8")) as {
version?: string;
};
if (!version) throw new Error(`${source} has no "version" field`);
return version;
}

/** Substitute the package version into a computed skill file. Applied only when
* writing — the manifest hashes the unstamped content, so a changesets version
* bump never registers as skill drift. Throws rather than ship a literal
* placeholder, which would tell every consumer their copy is stale. */
export function stampSkill(content: string, version: string | null): string {
if (version !== null) return content.replace(VERSION_PLACEHOLDER, version);
if (content.includes("{{APP_SHELL_VERSION}}")) {
throw new Error("skill template uses {{APP_SHELL_VERSION}} but skill.versionSource is not set");
}
return content;
}

/** Compute the skill and stamp it, so a bad version source fails before any write. */
export function computeStampedSkill(
repoRoot: string,
config: DocsConfig,
outlines: Outline[],
): Map<string, { content: string; stamped: string }> {
const version = readSkillVersion(repoRoot, config);
const files = new Map<string, { content: string; stamped: string }>();
for (const [p, content] of computeSkill(repoRoot, config, outlines)) {
files.set(p, { content, stamped: stampSkill(content, version) });
}
return files;
}

/** Compute the whole consumer skill as a map of repo-relative path → content.
* Pure: reads only committed inputs (the generated docs incl. concepts, the SKILL template, and the migrations doc) and writes nothing — so `sync`
* can emit it and `check` can recompute and validate it without the (gitignored)
Expand Down
7 changes: 4 additions & 3 deletions packages/docs-kit/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { type Finding, reconcile } from "./coverage";
import { hash, normalizeText } from "./hash";
import { writeManifest } from "./manifest";
import { discoverOutlines } from "./outline";
import { computeSkill } from "./skill";
import { computeStampedSkill } from "./skill";
import { loadSurface, snapshotHashForSlug } from "./project";
import type { Manifest, ManifestEntry } from "./types";

Expand Down Expand Up @@ -104,12 +104,13 @@ export function sync(repoRoot: string): SyncResult {
// docs + authored guidance, and hash each file so `check` can validate it.
let skill: Record<string, string> | undefined;
if (config.skill) {
const files = computeStampedSkill(repoRoot, config, outlines);
rmSync(join(repoRoot, config.skill.outDir), { recursive: true, force: true });
skill = {};
for (const [skillRel, content] of computeSkill(repoRoot, config, outlines)) {
for (const [skillRel, { content, stamped }] of files) {
const abs = join(repoRoot, skillRel);
mkdirSync(dirname(abs), { recursive: true });
writeFileSync(abs, content, "utf8");
writeFileSync(abs, stamped, "utf8");
skill[skillRel] = hash(normalizeText(content));
written.push(skillRel);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/docs-kit/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export interface SkillConfig {
migrationsSource: string;
/** Base blob URL for rewriting the migrations doc's relative links. */
repoBlobUrl: string;
/** Repo-relative package.json whose `version` is stamped into SKILL.md's
* `{{APP_SHELL_VERSION}}` at write time. */
versionSource?: string;
}

export interface DocsConfig {
Expand Down
Loading