@cosmicdrift/kumiko-guards 0.398.0 → 0.400.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/package.json +4 -4
- package/src/_lib/agent-docs-api-nodes.ts +107 -0
- package/src/_lib/agent-docs-api-render.ts +257 -0
- package/src/_lib/agent-docs-api.ts +285 -0
- package/src/_lib/agent-docs-feature-imports.ts +175 -0
- package/src/_lib/agent-docs-features.ts +196 -0
- package/src/_lib/agent-docs-module-walk.ts +103 -0
- package/src/_lib/agent-docs-shared.ts +76 -0
- package/src/_lib/agent-docs.ts +19 -205
- package/src/guard-agent-docs-drift.ts +2 -1
- package/src/guard-screen-conventions.ts +11 -2
package/src/_lib/agent-docs.ts
CHANGED
|
@@ -9,201 +9,26 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
|
-
import { type ExportedDeclarations, type JSDocableNode, Node, Project
|
|
12
|
+
import { type ExportedDeclarations, type JSDocableNode, Node, type Project } from "ts-morph";
|
|
13
|
+
import { renderApiDocs } from "./agent-docs-api";
|
|
14
|
+
import { renderFeatureDocs } from "./agent-docs-features";
|
|
15
|
+
import {
|
|
16
|
+
compareCodepoint,
|
|
17
|
+
createAgentDocsProject,
|
|
18
|
+
finishFile,
|
|
19
|
+
firstSentence,
|
|
20
|
+
isRecord,
|
|
21
|
+
parseJsonFile,
|
|
22
|
+
table,
|
|
23
|
+
} from "./agent-docs-shared";
|
|
13
24
|
|
|
14
25
|
export const AGENT_DOCS_DIR = "packages/framework/agents";
|
|
15
26
|
|
|
16
|
-
const FEATURE_MANIFEST_REL = "samples/apps/use-all-bundled/feature-manifest.json";
|
|
17
27
|
const RENDERER_WEB_INDEX_REL = "packages/renderer/src/web/index.ts";
|
|
18
28
|
const RECIPES_REL = "samples/recipes";
|
|
19
29
|
const RECIPE_SLUG_RE = /^[a-z0-9-]+$/;
|
|
20
30
|
const SOURCE_EXTENSIONS = [".ts", ".tsx"];
|
|
21
31
|
|
|
22
|
-
type ManifestConfigKeyDoc = {
|
|
23
|
-
readonly key: string;
|
|
24
|
-
readonly type: string;
|
|
25
|
-
readonly scope: string;
|
|
26
|
-
readonly default: string | number | boolean | null;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type ManifestSecretDoc = {
|
|
30
|
-
readonly qualifiedName: string;
|
|
31
|
-
readonly scope: string;
|
|
32
|
-
readonly label: string | null;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
type ManifestFeatureDoc = {
|
|
36
|
-
readonly name: string;
|
|
37
|
-
readonly description: string | null;
|
|
38
|
-
readonly category: string | null;
|
|
39
|
-
readonly requires: readonly string[];
|
|
40
|
-
readonly optionalRequires: readonly string[];
|
|
41
|
-
readonly exposesApis: readonly string[];
|
|
42
|
-
readonly usesApis: readonly string[];
|
|
43
|
-
readonly configKeys: readonly ManifestConfigKeyDoc[];
|
|
44
|
-
readonly secrets: readonly ManifestSecretDoc[];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isStringArray(value: unknown): value is readonly string[] {
|
|
52
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isNullableString(value: unknown): value is string | null {
|
|
56
|
-
return value === null || typeof value === "string";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isConfigKeyDoc(value: unknown): value is ManifestConfigKeyDoc {
|
|
60
|
-
if (!isRecord(value)) return false;
|
|
61
|
-
const fallback = value["default"];
|
|
62
|
-
return (
|
|
63
|
-
typeof value["key"] === "string" &&
|
|
64
|
-
typeof value["type"] === "string" &&
|
|
65
|
-
typeof value["scope"] === "string" &&
|
|
66
|
-
(fallback === null ||
|
|
67
|
-
typeof fallback === "string" ||
|
|
68
|
-
typeof fallback === "number" ||
|
|
69
|
-
typeof fallback === "boolean")
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function isSecretDoc(value: unknown): value is ManifestSecretDoc {
|
|
74
|
-
return (
|
|
75
|
-
isRecord(value) &&
|
|
76
|
-
typeof value["qualifiedName"] === "string" &&
|
|
77
|
-
typeof value["scope"] === "string" &&
|
|
78
|
-
isNullableString(value["label"])
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function parseFeatureDoc(value: unknown): ManifestFeatureDoc | undefined {
|
|
83
|
-
if (!isRecord(value) || typeof value["name"] !== "string") return undefined;
|
|
84
|
-
const description = value["description"];
|
|
85
|
-
const configKeys = value["configKeys"];
|
|
86
|
-
const secrets = value["secrets"];
|
|
87
|
-
const uiHints = value["uiHints"];
|
|
88
|
-
const category = isRecord(uiHints) ? uiHints["category"] : undefined;
|
|
89
|
-
if (
|
|
90
|
-
!isNullableString(description) ||
|
|
91
|
-
!isStringArray(value["requires"]) ||
|
|
92
|
-
!isStringArray(value["optionalRequires"]) ||
|
|
93
|
-
!isStringArray(value["exposesApis"]) ||
|
|
94
|
-
!isStringArray(value["usesApis"]) ||
|
|
95
|
-
!Array.isArray(configKeys) ||
|
|
96
|
-
!configKeys.every(isConfigKeyDoc) ||
|
|
97
|
-
!Array.isArray(secrets) ||
|
|
98
|
-
!secrets.every(isSecretDoc)
|
|
99
|
-
) {
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
name: value["name"],
|
|
104
|
-
description,
|
|
105
|
-
category: typeof category === "string" ? category : null,
|
|
106
|
-
requires: value["requires"],
|
|
107
|
-
optionalRequires: value["optionalRequires"],
|
|
108
|
-
exposesApis: value["exposesApis"],
|
|
109
|
-
usesApis: value["usesApis"],
|
|
110
|
-
configKeys,
|
|
111
|
-
secrets,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function parseJsonFile(relPath: string, raw: string): unknown {
|
|
116
|
-
try {
|
|
117
|
-
return JSON.parse(raw);
|
|
118
|
-
} catch (error) {
|
|
119
|
-
throw new Error(`${relPath}: invalid JSON`, { cause: error });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function parseFeatureManifest(raw: string): readonly ManifestFeatureDoc[] {
|
|
124
|
-
const parsed = parseJsonFile(FEATURE_MANIFEST_REL, raw);
|
|
125
|
-
const features = isRecord(parsed) ? parsed["features"] : undefined;
|
|
126
|
-
if (!Array.isArray(features)) {
|
|
127
|
-
throw new Error(`${FEATURE_MANIFEST_REL}: "features" must be an array`);
|
|
128
|
-
}
|
|
129
|
-
return features.map((entry, index) => {
|
|
130
|
-
const feature = parseFeatureDoc(entry);
|
|
131
|
-
if (feature === undefined) {
|
|
132
|
-
throw new Error(`${FEATURE_MANIFEST_REL}: features[${index}] has an unexpected shape`);
|
|
133
|
-
}
|
|
134
|
-
return feature;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function compareCodepoint(left: string, right: string): number {
|
|
139
|
-
if (left < right) return -1;
|
|
140
|
-
return left > right ? 1 : 0;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function generatedHeader(source: string): string {
|
|
144
|
-
return `<!-- Generated by \`bun run gen:agent-docs\` from ${source}. Do not edit. -->`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function finishFile(source: string, body: string): string {
|
|
148
|
-
const normalized = body.replace(/\r\n/g, "\n").trim();
|
|
149
|
-
return `${generatedHeader(source)}\n\n${normalized}\n`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function escapeCell(text: string): string {
|
|
153
|
-
return text.replace(/\s+/g, " ").trim().replace(/\|/g, "\\|");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function table(headers: readonly string[], rows: readonly (readonly string[])[]): string {
|
|
157
|
-
const lines = [
|
|
158
|
-
`| ${headers.join(" | ")} |`,
|
|
159
|
-
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
160
|
-
...rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`),
|
|
161
|
-
];
|
|
162
|
-
return lines.join("\n");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function codeList(values: readonly string[]): string {
|
|
166
|
-
return values.map((value) => `\`${value}\``).join(", ");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function renderFeatureSection(feature: ManifestFeatureDoc): string {
|
|
170
|
-
const parts = [`## ${feature.name}`];
|
|
171
|
-
if (feature.description !== null) parts.push(feature.description.replace(/\s+/g, " ").trim());
|
|
172
|
-
if (feature.category !== null) parts.push(`Category: ${feature.category}`);
|
|
173
|
-
if (feature.requires.length > 0) parts.push(`Requires: ${codeList(feature.requires)}`);
|
|
174
|
-
if (feature.optionalRequires.length > 0) {
|
|
175
|
-
parts.push(`Optional requires: ${codeList(feature.optionalRequires)}`);
|
|
176
|
-
}
|
|
177
|
-
if (feature.configKeys.length > 0) {
|
|
178
|
-
const rows = feature.configKeys.map((configKey) => [
|
|
179
|
-
`\`${configKey.key}\``,
|
|
180
|
-
configKey.type,
|
|
181
|
-
configKey.scope,
|
|
182
|
-
configKey.default === null ? "" : `\`${String(configKey.default)}\``,
|
|
183
|
-
]);
|
|
184
|
-
parts.push(`Config keys:\n\n${table(["Key", "Type", "Scope", "Default"], rows)}`);
|
|
185
|
-
}
|
|
186
|
-
if (feature.secrets.length > 0) {
|
|
187
|
-
const rows = feature.secrets.map((secret) => [
|
|
188
|
-
`\`${secret.qualifiedName}\``,
|
|
189
|
-
secret.scope,
|
|
190
|
-
secret.label ?? "",
|
|
191
|
-
]);
|
|
192
|
-
parts.push(`Secrets:\n\n${table(["Secret", "Scope", "Label"], rows)}`);
|
|
193
|
-
}
|
|
194
|
-
if (feature.exposesApis.length > 0) parts.push(`Exposes APIs: ${codeList(feature.exposesApis)}`);
|
|
195
|
-
if (feature.usesApis.length > 0) parts.push(`Uses APIs: ${codeList(feature.usesApis)}`);
|
|
196
|
-
return parts.join("\n\n");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function renderFeatures(repoRoot: string): string {
|
|
200
|
-
const raw = readFileSync(join(repoRoot, FEATURE_MANIFEST_REL), "utf-8");
|
|
201
|
-
const sections = [...parseFeatureManifest(raw)]
|
|
202
|
-
.sort((left, right) => compareCodepoint(left.name, right.name))
|
|
203
|
-
.map(renderFeatureSection);
|
|
204
|
-
return finishFile(FEATURE_MANIFEST_REL, `# Bundled features\n\n${sections.join("\n\n")}`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
32
|
type WidgetGroup = "Components" | "Hooks" | "Functions" | "Constants" | "Classes";
|
|
208
33
|
const WIDGET_GROUP_ORDER: readonly WidgetGroup[] = [
|
|
209
34
|
"Components",
|
|
@@ -241,13 +66,6 @@ function jsDocOwnerOf(declaration: ExportedDeclarations): JSDocableNode | undefi
|
|
|
241
66
|
return Node.isJSDocable(declaration) ? declaration : undefined;
|
|
242
67
|
}
|
|
243
68
|
|
|
244
|
-
const FIRST_SENTENCE_RE = /^(.+?(?<!\be\.g|\bi\.e)[.!?])(?:\s|$)/;
|
|
245
|
-
|
|
246
|
-
function firstSentence(text: string): string {
|
|
247
|
-
const flattened = text.replace(/\s+/g, " ").trim();
|
|
248
|
-
return FIRST_SENTENCE_RE.exec(flattened)?.[1] ?? flattened;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
69
|
function descriptionOf(declarations: readonly ExportedDeclarations[]): string {
|
|
252
70
|
for (const declaration of declarations) {
|
|
253
71
|
const docs = jsDocOwnerOf(declaration)?.getJsDocs() ?? [];
|
|
@@ -257,15 +75,7 @@ function descriptionOf(declarations: readonly ExportedDeclarations[]): string {
|
|
|
257
75
|
return "";
|
|
258
76
|
}
|
|
259
77
|
|
|
260
|
-
function renderWidgets(repoRoot: string): string {
|
|
261
|
-
const project = new Project({
|
|
262
|
-
compilerOptions: {
|
|
263
|
-
module: ts.ModuleKind.ESNext,
|
|
264
|
-
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
265
|
-
target: ts.ScriptTarget.ES2022,
|
|
266
|
-
jsx: ts.JsxEmit.ReactJSX,
|
|
267
|
-
},
|
|
268
|
-
});
|
|
78
|
+
function renderWidgets(repoRoot: string, project: Project): string {
|
|
269
79
|
const indexFile = project.addSourceFileAtPath(join(repoRoot, RENDERER_WEB_INDEX_REL));
|
|
270
80
|
const grouped = new Map<WidgetGroup, [string, string][]>(
|
|
271
81
|
WIDGET_GROUP_ORDER.map((group) => [group, []]),
|
|
@@ -381,9 +191,13 @@ function renderRecipe(recipe: Recipe): string {
|
|
|
381
191
|
|
|
382
192
|
export function renderAgentDocs(repoRoot: string): ReadonlyMap<string, string> {
|
|
383
193
|
const recipes = readRecipes(repoRoot);
|
|
194
|
+
const project = createAgentDocsProject(repoRoot);
|
|
195
|
+
const featureDocs = renderFeatureDocs(repoRoot, project);
|
|
384
196
|
const files = new Map<string, string>();
|
|
385
|
-
files.set("features.md",
|
|
386
|
-
files.set("
|
|
197
|
+
files.set("features-index.md", featureDocs.featuresIndex);
|
|
198
|
+
files.set("features.md", featureDocs.features);
|
|
199
|
+
files.set("widgets.md", renderWidgets(repoRoot, project));
|
|
200
|
+
for (const [rel, content] of renderApiDocs(repoRoot, project)) files.set(rel, content);
|
|
387
201
|
files.set("examples.md", renderExamplesIndex(recipes));
|
|
388
202
|
for (const recipe of recipes) files.set(`examples/${recipe.slug}.md`, renderRecipe(recipe));
|
|
389
203
|
return files;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Guard: the generated agent docs under `packages/framework/agents/` must
|
|
4
4
|
* match what `bun run gen:agent-docs` renders from the current sources
|
|
5
|
-
* (feature manifest, renderer web exports,
|
|
5
|
+
* (feature manifest, public entry-point signatures, renderer web exports,
|
|
6
|
+
* sample recipes), and every
|
|
6
7
|
* `agents/<file>.md` path the hand-written `packages/framework/AGENTS.md`
|
|
7
8
|
* points at must exist in that generated set.
|
|
8
9
|
*
|
|
@@ -48,6 +48,14 @@ function stringLiteralValue(obj: ObjectLiteralExpression, name: string): string
|
|
|
48
48
|
return init?.isKind(SyntaxKind.StringLiteral) ? init.getLiteralValue() : undefined;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function booleanLiteralValue(obj: ObjectLiteralExpression, name: string): boolean | undefined {
|
|
52
|
+
const prop = obj.getProperty(name);
|
|
53
|
+
if (!prop?.isKind(SyntaxKind.PropertyAssignment)) return undefined;
|
|
54
|
+
const init = prop.getInitializer();
|
|
55
|
+
if (init?.isKind(SyntaxKind.TrueKeyword)) return true;
|
|
56
|
+
return init?.isKind(SyntaxKind.FalseKeyword) ? false : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
function hasDeclaredProperty(obj: ObjectLiteralExpression, name: string): boolean {
|
|
52
60
|
const prop = obj.getProperty(name);
|
|
53
61
|
if (!prop?.isKind(SyntaxKind.PropertyAssignment)) return false;
|
|
@@ -55,7 +63,7 @@ function hasDeclaredProperty(obj: ObjectLiteralExpression, name: string): boolea
|
|
|
55
63
|
return init !== undefined && init.getKind() !== SyntaxKind.UndefinedKeyword;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
// R2: a relatedList section with neither rowClick nor
|
|
66
|
+
// R2: a relatedList section with neither rowClick, rowActions nor readOnly renders a
|
|
59
67
|
// list nobody can act on.
|
|
60
68
|
function checkRelatedListSections(
|
|
61
69
|
root: ObjectLiteralExpression,
|
|
@@ -65,6 +73,7 @@ function checkRelatedListSections(
|
|
|
65
73
|
for (const obj of root.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
66
74
|
if (stringLiteralValue(obj, "kind") !== "relatedList") continue;
|
|
67
75
|
if (hasIgnoreTag(obj, IGNORE_TAG)) continue;
|
|
76
|
+
if (booleanLiteralValue(obj, "readOnly") === true) continue;
|
|
68
77
|
if (hasDeclaredProperty(obj, "rowClick") || hasDeclaredProperty(obj, "rowActions")) continue;
|
|
69
78
|
violations.push({
|
|
70
79
|
file: sf.getFilePath(),
|
|
@@ -107,7 +116,7 @@ export const guard: AstGuard = {
|
|
|
107
116
|
name: "Screen-Conventions Guard",
|
|
108
117
|
scan: SCAN,
|
|
109
118
|
hint:
|
|
110
|
-
"relatedList sections need `rowClick` or `
|
|
119
|
+
"relatedList sections need `rowClick`, `rowActions` or `readOnly: true`; a rowAction label " +
|
|
111
120
|
'must not end in ".action.edit" ' +
|
|
112
121
|
'when the `id` is not "edit". ' +
|
|
113
122
|
`Justified exception: // ${IGNORE_TAG} <reason>`,
|