From 4393bf8e93f8bde912250d95ab5c95b4e91716d4 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 15 Mar 2026 19:34:44 +0000 Subject: [PATCH 1/6] fix: make trgm adapter supplementary and filter search fields from codegen docs - Add isSupplementary flag to SearchAdapter interface - trgm adapter now only activates on tables that already have intentional search (bm25 indexes, tsvector columns, pgvector embeddings) - Plugin's getAdapterColumns runs primary adapters first, then only runs supplementary adapters if primary adapters found columns - getEditableFields now accepts TypeRegistry to filter out computed/search fields - Add getSearchFields utility to identify plugin-added search fields - CLI docs generators pass registry to getEditableFields for proper filtering - Add 'Search API fields (computed, read-only)' callout in generated docs --- graphile/graphile-search/src/adapters/trgm.ts | 26 +++++++++++++++- graphile/graphile-search/src/plugin.ts | 31 ++++++++++++++++--- graphile/graphile-search/src/types.ts | 12 +++++++ .../src/core/codegen/cli/docs-generator.ts | 29 +++++++++++------ .../codegen/src/core/codegen/docs-utils.ts | 29 +++++++++++++++-- 5 files changed, 109 insertions(+), 18 deletions(-) diff --git a/graphile/graphile-search/src/adapters/trgm.ts b/graphile/graphile-search/src/adapters/trgm.ts index 8248048ce..aa50508b8 100644 --- a/graphile/graphile-search/src/adapters/trgm.ts +++ b/graphile/graphile-search/src/adapters/trgm.ts @@ -25,16 +25,40 @@ export interface TrgmAdapterOptions { * @default 0.3 */ defaultThreshold?: number; + + /** + * When true, trgm only activates on tables that have an "intentional" + * search column detected by another adapter (e.g. a tsvector column or + * a BM25 index). This prevents trgm similarity fields from being added + * to every table with text columns. + * + * The plugin's `getAdapterColumns` orchestrates this by running + * non-supplementary adapters first, then only running supplementary + * adapters on codecs that already have search columns. + * + * @default true + */ + requireIntentionalSearch?: boolean; } export function createTrgmAdapter( options: TrgmAdapterOptions = {} ): SearchAdapter { - const { filterPrefix = 'trgm', defaultThreshold = 0.3 } = options; + const { + filterPrefix = 'trgm', + defaultThreshold = 0.3, + requireIntentionalSearch = true, + } = options; return { name: 'trgm', + /** + * When true, this adapter is "supplementary" — it only activates on + * tables that already have columns detected by a non-supplementary adapter. + */ + isSupplementary: requireIntentionalSearch, + scoreSemantics: { metric: 'similarity', lowerIsBetter: false, diff --git a/graphile/graphile-search/src/plugin.ts b/graphile/graphile-search/src/plugin.ts index e20d337bb..e23464443 100644 --- a/graphile/graphile-search/src/plugin.ts +++ b/graphile/graphile-search/src/plugin.ts @@ -88,6 +88,11 @@ export function createUnifiedSearchPlugin( /** * Get (or compute) the adapter columns for a given codec. + * + * Runs non-supplementary adapters first (e.g. tsvector, BM25, pgvector). + * Supplementary adapters (e.g. trgm with requireIntentionalSearch) are only + * run if at least one non-supplementary adapter found columns — this prevents + * trgm from adding similarity fields to every table with text columns. */ function getAdapterColumns(codec: PgCodecWithAttributes, build: any): AdapterColumnCache[] { const cacheKey = codec.name; @@ -95,13 +100,29 @@ export function createUnifiedSearchPlugin( return codecCache.get(cacheKey)!; } + const primaryAdapters = adapters.filter((a) => !a.isSupplementary); + const supplementaryAdapters = adapters.filter((a) => a.isSupplementary); + + // Phase 1: Run non-supplementary (intentional search) adapters const results: AdapterColumnCache[] = []; - for (const adapter of adapters) { + for (const adapter of primaryAdapters) { const columns = adapter.detectColumns(codec, build); if (columns.length > 0) { results.push({ adapter, columns }); } } + + // Phase 2: Only run supplementary adapters if at least one primary + // adapter found columns on this codec (i.e. intentional search exists) + if (results.length > 0) { + for (const adapter of supplementaryAdapters) { + const columns = adapter.detectColumns(codec, build); + if (columns.length > 0) { + results.push({ adapter, columns }); + } + } + } + codecCache.set(cacheKey, results); return results; } @@ -170,9 +191,11 @@ export function createUnifiedSearchPlugin( provides: ['default'], before: ['inferred', 'override', 'PgAttributesPlugin'], callback(behavior, [codec, attributeName], build) { - // Check if any adapter claims this column - for (const adapter of adapters) { - const columns = adapter.detectColumns(codec, build); + // Use getAdapterColumns which respects isSupplementary logic, + // so trgm columns only appear when intentional search exists + if (!codec?.attributes) return behavior; + const adapterColumns = getAdapterColumns(codec as PgCodecWithAttributes, build); + for (const { columns } of adapterColumns) { if (columns.some((c) => c.attributeName === attributeName)) { return [ 'unifiedSearch:orderBy', diff --git a/graphile/graphile-search/src/types.ts b/graphile/graphile-search/src/types.ts index 1e41c1d55..ccbf00b90 100644 --- a/graphile/graphile-search/src/types.ts +++ b/graphile/graphile-search/src/types.ts @@ -75,6 +75,18 @@ export interface SearchAdapter { /** Score semantics for this algorithm. */ scoreSemantics: ScoreSemantics; + /** + * When true, this adapter is "supplementary" — it only activates on + * tables that already have at least one column detected by a + * non-supplementary adapter (e.g. tsvector or BM25). + * + * This prevents adapters like pg_trgm from adding similarity fields + * to every table with text columns when there is no intentional search setup. + * + * @default false + */ + isSupplementary?: boolean; + /** * The filter prefix used for filter field names on the connection filter input. * The field name is: `{filterPrefix}{ColumnName}` (camelCase). diff --git a/graphql/codegen/src/core/codegen/cli/docs-generator.ts b/graphql/codegen/src/core/codegen/cli/docs-generator.ts index 049b32b4b..ef5ba0a7b 100644 --- a/graphql/codegen/src/core/codegen/cli/docs-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/docs-generator.ts @@ -6,6 +6,7 @@ import { flattenedArgsToFlags, cleanTypeName, getEditableFields, + getSearchFields, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, @@ -112,7 +113,7 @@ export function generateReadme( const kebab = toKebabCase(singularName); const pk = getPrimaryKeyInfo(table)[0]; const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); lines.push(`### \`${kebab}\``); lines.push(''); @@ -146,6 +147,10 @@ export function generateReadme( if (requiredCreate.length === 0 && optionalCreate.length === 0) { lines.push(`**Create fields:** ${editableFields.map((f) => `\`${f.name}\``).join(', ')}`); } + const searchFields = getSearchFields(table, registry); + if (searchFields.length > 0) { + lines.push(`**Search API fields (computed, read-only):** ${searchFields.map((f) => `\`${f.name}\``).join(', ')}`); + } lines.push(''); } } @@ -309,7 +314,7 @@ export function generateAgentsDocs( const kebab = toKebabCase(singularName); const pk = getPrimaryKeyInfo(table)[0]; const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); const defaultFields = getFieldsWithDefaults(table, registry); const requiredCreateFields = editableFields.filter((f) => !defaultFields.has(f.name)); const optionalCreateFields = editableFields.filter((f) => defaultFields.has(f.name)); @@ -397,7 +402,7 @@ export function generateAgentsDocs( const firstTable = tables[0]; const { singularName } = getTableNames(firstTable); const kebab = toKebabCase(singularName); - const editableFields = getEditableFields(firstTable); + const editableFields = getEditableFields(firstTable, registry); const pk = getPrimaryKeyInfo(firstTable)[0]; lines.push(`### CRUD workflow (${kebab})`); @@ -582,7 +587,7 @@ export function getCliMcpTools( const kebab = toKebabCase(singularName); const pk = getPrimaryKeyInfo(table)[0]; const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); const defaultFields = getFieldsWithDefaults(table, registry); const requiredCreateFieldNames = editableFields .filter((f) => !defaultFields.has(f.name)) @@ -808,7 +813,7 @@ export function generateSkills( const { singularName } = getTableNames(table); const kebab = toKebabCase(singularName); const pk = getPrimaryKeyInfo(table)[0]; - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); const defaultFields = getFieldsWithDefaults(table, registry); const createFlags = [ ...editableFields.filter((f) => !defaultFields.has(f.name)).map((f) => `--${f.name} `), @@ -1117,7 +1122,7 @@ export function generateMultiTargetReadme( const kebab = toKebabCase(singularName); const pk = getPrimaryKeyInfo(table)[0]; const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); const defaultFields = getFieldsWithDefaults(table, registry); lines.push(`### \`${tgt.name}:${kebab}\``); @@ -1151,6 +1156,10 @@ export function generateMultiTargetReadme( if (requiredCreate.length === 0 && optionalCreate.length === 0) { lines.push(`**Create fields:** ${editableFields.map((f) => `\`${f.name}\``).join(', ')}`); } + const searchFields = getSearchFields(table, registry); + if (searchFields.length > 0) { + lines.push(`**Search API fields (computed, read-only):** ${searchFields.map((f) => `\`${f.name}\``).join(', ')}`); + } lines.push(''); } @@ -1365,7 +1374,7 @@ export function generateMultiTargetAgentsDocs( const kebab = toKebabCase(singularName); const pk = getPrimaryKeyInfo(table)[0]; const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); const defaultFields = getFieldsWithDefaults(table, registry); const requiredCreateFields = editableFields.filter((f) => !defaultFields.has(f.name)); const optionalCreateFields = editableFields.filter((f) => defaultFields.has(f.name)); @@ -1473,7 +1482,7 @@ export function generateMultiTargetAgentsDocs( const table = tgt.tables[0]; const { singularName } = getTableNames(table); const kebab = toKebabCase(singularName); - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); const pk = getPrimaryKeyInfo(table)[0]; lines.push(`### CRUD workflow (${tgt.name}:${kebab})`); @@ -1654,7 +1663,7 @@ export function getMultiTargetCliMcpTools( const kebab = toKebabCase(singularName); const pk = getPrimaryKeyInfo(table)[0]; const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); const defaultFields = getFieldsWithDefaults(table, registry); const requiredCreateFieldNames = editableFields .filter((f) => !defaultFields.has(f.name)) @@ -1941,7 +1950,7 @@ export function generateMultiTargetSkills( const { singularName } = getTableNames(table); const kebab = toKebabCase(singularName); const pk = getPrimaryKeyInfo(table)[0]; - const editableFields = getEditableFields(table); + const editableFields = getEditableFields(table, registry); const defaultFields = getFieldsWithDefaults(table, registry); const createFlags = [ ...editableFields.filter((f) => !defaultFields.has(f.name)).map((f) => `--${f.name} `), diff --git a/graphql/codegen/src/core/codegen/docs-utils.ts b/graphql/codegen/src/core/codegen/docs-utils.ts index 33ecf6ef8..0bee217c1 100644 --- a/graphql/codegen/src/core/codegen/docs-utils.ts +++ b/graphql/codegen/src/core/codegen/docs-utils.ts @@ -1,6 +1,6 @@ import type { DocsConfig } from '../../types/config'; import type { CleanArgument, CleanField, CleanOperation, CleanTable, CleanTypeRef, TypeRegistry } from '../../types/schema'; -import { getScalarFields, getPrimaryKeyInfo } from './utils'; +import { getScalarFields, getPrimaryKeyInfo, getWritableFieldNames } from './utils'; export interface GeneratedDocFile { fileName: string; @@ -103,14 +103,37 @@ export function formatTypeRef( return t.name ?? 'unknown'; } -export function getEditableFields(table: CleanTable): CleanField[] { +export function getEditableFields(table: CleanTable, typeRegistry?: TypeRegistry): CleanField[] { const pk = getPrimaryKeyInfo(table)[0]; + const writableFields = getWritableFieldNames(table, typeRegistry); return getScalarFields(table).filter( (f) => f.name !== pk.name && f.name !== 'nodeId' && f.name !== 'createdAt' && - f.name !== 'updatedAt', + f.name !== 'updatedAt' && + // When a TypeRegistry is available, filter out computed/plugin-added + // fields (e.g. search scores, trgm similarity) that aren't real columns + (writableFields === null || writableFields.has(f.name)), + ); +} + +/** + * Identify search/computed fields on a table — fields present in the GraphQL + * type but NOT in the create input type. These are plugin-added fields like + * trgm similarity scores, tsvector ranks, searchScore, etc. + */ +export function getSearchFields(table: CleanTable, typeRegistry?: TypeRegistry): CleanField[] { + const writableFields = getWritableFieldNames(table, typeRegistry); + if (writableFields === null) return []; + const pk = getPrimaryKeyInfo(table)[0]; + return getScalarFields(table).filter( + (f) => + f.name !== pk.name && + f.name !== 'nodeId' && + f.name !== 'createdAt' && + f.name !== 'updatedAt' && + !writableFields.has(f.name), ); } From c74976461d70b7a40c37618e9625756892738fe7 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 15 Mar 2026 19:46:31 +0000 Subject: [PATCH 2/6] fix: pgvector should not trigger supplementary adapters like trgm pgvector operates on embedding vectors, not text search. Add isIntentionalSearch flag to SearchAdapter interface so that only adapters representing real search infrastructure (tsvector, BM25) trigger supplementary adapters. pgvector sets isIntentionalSearch: false. --- .../graphile-search/src/adapters/pgvector.ts | 4 ++++ graphile/graphile-search/src/plugin.ts | 21 ++++++++++++++----- graphile/graphile-search/src/types.ts | 21 +++++++++++++++++-- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/graphile/graphile-search/src/adapters/pgvector.ts b/graphile/graphile-search/src/adapters/pgvector.ts index 93286c8ca..ea40e5e21 100644 --- a/graphile/graphile-search/src/adapters/pgvector.ts +++ b/graphile/graphile-search/src/adapters/pgvector.ts @@ -52,6 +52,10 @@ export function createPgvectorAdapter( filterPrefix, + // pgvector operates on embedding vectors, not text search — its presence + // alone should NOT trigger supplementary adapters like trgm. + isIntentionalSearch: false, + supportsTextSearch: false, // pgvector requires a vector array, not plain text — no buildTextSearchInput diff --git a/graphile/graphile-search/src/plugin.ts b/graphile/graphile-search/src/plugin.ts index e23464443..0758705d9 100644 --- a/graphile/graphile-search/src/plugin.ts +++ b/graphile/graphile-search/src/plugin.ts @@ -91,8 +91,12 @@ export function createUnifiedSearchPlugin( * * Runs non-supplementary adapters first (e.g. tsvector, BM25, pgvector). * Supplementary adapters (e.g. trgm with requireIntentionalSearch) are only - * run if at least one non-supplementary adapter found columns — this prevents - * trgm from adding similarity fields to every table with text columns. + * run if at least one adapter with `isIntentionalSearch: true` found columns. + * + * This distinction matters because pgvector (embeddings) is NOT intentional + * text search — its presence alone should not trigger trgm similarity fields. + * Only tsvector and BM25, which represent explicit search infrastructure, + * count as intentional search. */ function getAdapterColumns(codec: PgCodecWithAttributes, build: any): AdapterColumnCache[] { const cacheKey = codec.name; @@ -103,18 +107,25 @@ export function createUnifiedSearchPlugin( const primaryAdapters = adapters.filter((a) => !a.isSupplementary); const supplementaryAdapters = adapters.filter((a) => a.isSupplementary); - // Phase 1: Run non-supplementary (intentional search) adapters + // Phase 1: Run non-supplementary adapters (tsvector, BM25, pgvector, etc.) const results: AdapterColumnCache[] = []; + let hasIntentionalSearch = false; for (const adapter of primaryAdapters) { const columns = adapter.detectColumns(codec, build); if (columns.length > 0) { results.push({ adapter, columns }); + // Track whether any "intentional search" adapter found columns. + // isIntentionalSearch defaults to true when not explicitly set. + if (adapter.isIntentionalSearch !== false) { + hasIntentionalSearch = true; + } } } // Phase 2: Only run supplementary adapters if at least one primary - // adapter found columns on this codec (i.e. intentional search exists) - if (results.length > 0) { + // adapter with isIntentionalSearch found columns on this codec. + // pgvector (isIntentionalSearch: false) alone won't trigger trgm. + if (hasIntentionalSearch) { for (const adapter of supplementaryAdapters) { const columns = adapter.detectColumns(codec, build); if (columns.length > 0) { diff --git a/graphile/graphile-search/src/types.ts b/graphile/graphile-search/src/types.ts index ccbf00b90..5d9f49d53 100644 --- a/graphile/graphile-search/src/types.ts +++ b/graphile/graphile-search/src/types.ts @@ -77,16 +77,33 @@ export interface SearchAdapter { /** * When true, this adapter is "supplementary" — it only activates on - * tables that already have at least one column detected by a - * non-supplementary adapter (e.g. tsvector or BM25). + * tables that already have at least one column detected by an adapter + * whose `isIntentionalSearch` is true (e.g. tsvector or BM25). * * This prevents adapters like pg_trgm from adding similarity fields * to every table with text columns when there is no intentional search setup. * + * pgvector (embeddings) does NOT count as intentional search because it + * operates on vector columns, not text search — so its presence alone + * won't trigger supplementary adapters. + * * @default false */ isSupplementary?: boolean; + /** + * When true, this adapter represents "intentional search" — its presence + * on a table signals that the table was explicitly set up for search and + * should trigger supplementary adapters (e.g. trgm). + * + * Adapters that check for real infrastructure (tsvector columns, BM25 + * indexes) should set this to true. Adapters that operate on a different + * domain (pgvector embeddings) should set this to false. + * + * @default true + */ + isIntentionalSearch?: boolean; + /** * The filter prefix used for filter field names on the connection filter input. * The field name is: `{filterPrefix}{ColumnName}` (camelCase). From e8c5508479890bd1a8212d013c3ff6ed0cfef7f1 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 15 Mar 2026 19:57:35 +0000 Subject: [PATCH 3/6] feat: add PostGIS, pgvector, and Unified Search field sections to all generated docs --- .../src/core/codegen/cli/docs-generator.ts | 47 ++++-- .../codegen/src/core/codegen/docs-utils.ts | 144 ++++++++++++++++++ .../src/core/codegen/orm/docs-generator.ts | 22 ++- 3 files changed, 202 insertions(+), 11 deletions(-) diff --git a/graphql/codegen/src/core/codegen/cli/docs-generator.ts b/graphql/codegen/src/core/codegen/cli/docs-generator.ts index ef5ba0a7b..685ad5439 100644 --- a/graphql/codegen/src/core/codegen/cli/docs-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/docs-generator.ts @@ -7,6 +7,9 @@ import { cleanTypeName, getEditableFields, getSearchFields, + categorizeSpecialFields, + buildSpecialFieldsMarkdown, + buildSpecialFieldsPlain, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, @@ -147,10 +150,8 @@ export function generateReadme( if (requiredCreate.length === 0 && optionalCreate.length === 0) { lines.push(`**Create fields:** ${editableFields.map((f) => `\`${f.name}\``).join(', ')}`); } - const searchFields = getSearchFields(table, registry); - if (searchFields.length > 0) { - lines.push(`**Search API fields (computed, read-only):** ${searchFields.map((f) => `\`${f.name}\``).join(', ')}`); - } + const specialGroups = categorizeSpecialFields(table, registry); + lines.push(...buildSpecialFieldsMarkdown(specialGroups)); lines.push(''); } } @@ -347,6 +348,14 @@ export function generateAgentsDocs( lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}${optLabel}`); } lines.push(''); + const agentSpecialGroups = categorizeSpecialFields(table, registry); + const agentSpecialLines = buildSpecialFieldsPlain(agentSpecialGroups); + if (agentSpecialLines.length > 0) { + for (const sl of agentSpecialLines) { + lines.push(sl); + } + lines.push(''); + } lines.push('OUTPUT: JSON'); lines.push(` list: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`); lines.push(` get: { ${scalarFields.map((f) => f.name).join(', ')} }`); @@ -822,11 +831,17 @@ export function generateSkills( referenceNames.push(kebab); + const skillSpecialGroups = categorizeSpecialFields(table, registry); + const skillSpecialDesc = skillSpecialGroups.length > 0 + ? `CRUD operations for ${table.name} records via ${toolName} CLI\n\n` + + skillSpecialGroups.map((g) => `**${g.label}:** ${g.fields.map((f) => `\`${f.name}\``).join(', ')}\n${g.description}`).join('\n\n') + : `CRUD operations for ${table.name} records via ${toolName} CLI`; + files.push({ fileName: `${skillName}/references/${kebab}.md`, content: buildSkillReference({ title: singularName, - description: `CRUD operations for ${table.name} records via ${toolName} CLI`, + description: skillSpecialDesc, usage: [ `${toolName} ${kebab} list`, `${toolName} ${kebab} get --${pk.name} `, @@ -1156,10 +1171,8 @@ export function generateMultiTargetReadme( if (requiredCreate.length === 0 && optionalCreate.length === 0) { lines.push(`**Create fields:** ${editableFields.map((f) => `\`${f.name}\``).join(', ')}`); } - const searchFields = getSearchFields(table, registry); - if (searchFields.length > 0) { - lines.push(`**Search API fields (computed, read-only):** ${searchFields.map((f) => `\`${f.name}\``).join(', ')}`); - } + const mtSpecialGroups = categorizeSpecialFields(table, registry); + lines.push(...buildSpecialFieldsMarkdown(mtSpecialGroups)); lines.push(''); } @@ -1407,6 +1420,14 @@ export function generateMultiTargetAgentsDocs( lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}${optLabel}`); } lines.push(''); + const mtAgentSpecialGroups = categorizeSpecialFields(table, registry); + const mtAgentSpecialLines = buildSpecialFieldsPlain(mtAgentSpecialGroups); + if (mtAgentSpecialLines.length > 0) { + for (const sl of mtAgentSpecialLines) { + lines.push(sl); + } + lines.push(''); + } lines.push('OUTPUT: JSON'); lines.push(` list: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`); lines.push(` get: { ${scalarFields.map((f) => f.name).join(', ')} }`); @@ -1960,11 +1981,17 @@ export function generateMultiTargetSkills( tgtReferenceNames.push(kebab); + const mtSkillSpecialGroups = categorizeSpecialFields(table, registry); + const mtSkillSpecialDesc = mtSkillSpecialGroups.length > 0 + ? `CRUD operations for ${table.name} records via ${toolName} CLI (${tgt.name} target)\n\n` + + mtSkillSpecialGroups.map((g) => `**${g.label}:** ${g.fields.map((f) => `\`${f.name}\``).join(', ')}\n${g.description}`).join('\n\n') + : `CRUD operations for ${table.name} records via ${toolName} CLI (${tgt.name} target)`; + files.push({ fileName: `${tgtSkillName}/references/${kebab}.md`, content: buildSkillReference({ title: singularName, - description: `CRUD operations for ${table.name} records via ${toolName} CLI (${tgt.name} target)`, + description: mtSkillSpecialDesc, usage: [ `${toolName} ${cmd} list`, `${toolName} ${cmd} get --${pk.name} `, diff --git a/graphql/codegen/src/core/codegen/docs-utils.ts b/graphql/codegen/src/core/codegen/docs-utils.ts index 0bee217c1..a56db2d6b 100644 --- a/graphql/codegen/src/core/codegen/docs-utils.ts +++ b/graphql/codegen/src/core/codegen/docs-utils.ts @@ -137,6 +137,150 @@ export function getSearchFields(table: CleanTable, typeRegistry?: TypeRegistry): ); } +// --------------------------------------------------------------------------- +// Special field categorization — PostGIS, pgvector, Unified Search +// --------------------------------------------------------------------------- + +export interface SpecialFieldGroup { + /** Category key */ + category: 'geospatial' | 'embedding' | 'search'; + /** Human-readable label */ + label: string; + /** One-line description of this category */ + description: string; + /** Fields belonging to this category */ + fields: CleanField[]; +} + +function isPostGISField(f: CleanField): boolean { + const pgType = f.type.pgType?.toLowerCase(); + if (pgType === 'geometry' || pgType === 'geography') return true; + const gql = f.type.gqlType; + if (/^(GeoJSON|GeographyPoint|GeographyLineString|GeographyPolygon|GeometryPoint|GeometryLineString|GeometryPolygon|GeographyMulti|GeometryMulti|GeometryCollection|GeographyCollection)/i.test(gql)) return true; + return false; +} + +function isEmbeddingField(f: CleanField): boolean { + const pgType = f.type.pgType?.toLowerCase(); + if (pgType === 'vector') return true; + if (/embedding$/i.test(f.name) && f.type.isArray && f.type.gqlType === 'Float') return true; + return false; +} + +function isTsvectorField(f: CleanField): boolean { + const pgType = f.type.pgType?.toLowerCase(); + return pgType === 'tsvector'; +} + +function isSearchComputedField(f: CleanField): boolean { + if (f.name === 'searchScore') return true; + if (/TrgmSimilarity$/.test(f.name)) return true; + if (/TsvectorRank$/.test(f.name)) return true; + if (/Bm25Score$/.test(f.name)) return true; + return false; +} + +/** + * Categorize "special" fields on a table into PostGIS, pgvector, and + * Unified Search groups. Returns only non-empty groups. + * + * The function inspects ALL scalar fields (not just computed ones) so that + * real columns (geometry, vector, tsvector) are also surfaced with + * descriptive context in generated docs. + */ +export function categorizeSpecialFields( + table: CleanTable, + typeRegistry?: TypeRegistry, +): SpecialFieldGroup[] { + const allFields = getScalarFields(table); + const computedFields = getSearchFields(table, typeRegistry); + const computedSet = new Set(computedFields.map((f) => f.name)); + + const geospatial: CleanField[] = []; + const embedding: CleanField[] = []; + const search: CleanField[] = []; + + for (const f of allFields) { + if (isPostGISField(f)) { + geospatial.push(f); + } else if (isEmbeddingField(f)) { + embedding.push(f); + } else if (isTsvectorField(f)) { + search.push(f); + } else if (computedSet.has(f.name) && isSearchComputedField(f)) { + search.push(f); + } + } + + const groups: SpecialFieldGroup[] = []; + + if (geospatial.length > 0) { + groups.push({ + category: 'geospatial', + label: 'PostGIS geospatial fields', + description: + 'Geographic/geometric columns managed by PostGIS. Supports spatial queries (distance, containment, intersection) via the Unified Search API PostGIS adapter.', + fields: geospatial, + }); + } + + if (embedding.length > 0) { + groups.push({ + category: 'embedding', + label: 'pgvector embedding fields', + description: + 'High-dimensional vector columns for semantic similarity search. Query via the Unified Search API pgvector adapter using cosine, L2, or inner-product distance.', + fields: embedding, + }); + } + + if (search.length > 0) { + groups.push({ + category: 'search', + label: 'Unified Search API fields', + description: + 'Fields provided by the Unified Search plugin. Includes full-text search (tsvector/BM25), trigram similarity scores, and the combined searchScore. Computed fields are read-only and cannot be set in create/update operations.', + fields: search, + }); + } + + return groups; +} + +/** + * Build markdown lines describing special fields for README-style docs. + * Returns empty array when there are no special fields. + */ +export function buildSpecialFieldsMarkdown(groups: SpecialFieldGroup[]): string[] { + if (groups.length === 0) return []; + const lines: string[] = []; + for (const g of groups) { + const fieldList = g.fields.map((f) => `\`${f.name}\``).join(', '); + lines.push(`> **${g.label}:** ${fieldList}`); + lines.push(`> ${g.description}`); + lines.push(''); + } + return lines; +} + +/** + * Build plain-text lines describing special fields for AGENTS-style docs. + * Returns empty array when there are no special fields. + */ +export function buildSpecialFieldsPlain(groups: SpecialFieldGroup[]): string[] { + if (groups.length === 0) return []; + const lines: string[] = []; + lines.push('SPECIAL FIELDS:'); + for (const g of groups) { + lines.push(` [${g.label}]`); + lines.push(` ${g.description}`); + for (const f of g.fields) { + lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}`); + } + } + return lines; +} + /** * Represents a flattened argument for docs/skills generation. * INPUT_OBJECT args are expanded to dot-notation fields. diff --git a/graphql/codegen/src/core/codegen/orm/docs-generator.ts b/graphql/codegen/src/core/codegen/orm/docs-generator.ts index fb2c05798..0b6c20469 100644 --- a/graphql/codegen/src/core/codegen/orm/docs-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/docs-generator.ts @@ -6,6 +6,9 @@ import { buildSkillReference, formatArgType, getEditableFields, + categorizeSpecialFields, + buildSpecialFieldsMarkdown, + buildSpecialFieldsPlain, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, @@ -104,6 +107,8 @@ export function generateOrmReadme( ); lines.push('```'); lines.push(''); + const ormSpecialGroups = categorizeSpecialFields(table); + lines.push(...buildSpecialFieldsMarkdown(ormSpecialGroups)); } } @@ -228,6 +233,14 @@ export function generateOrmAgentsDocs( lines.push(` ${f.name}: ${fieldTypeToTs(f.type)}`); } lines.push(''); + const ormAgentSpecialGroups = categorizeSpecialFields(table); + const ormAgentSpecialLines = buildSpecialFieldsPlain(ormAgentSpecialGroups); + if (ormAgentSpecialLines.length > 0) { + for (const sl of ormAgentSpecialLines) { + lines.push(sl); + } + lines.push(''); + } lines.push('OUTPUT: Promise'); lines.push( ` findMany: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`, @@ -468,11 +481,18 @@ export function generateOrmSkills( const refName = toKebabCase(singularName); referenceNames.push(refName); + const ormSkillSpecialGroups = categorizeSpecialFields(table); + const ormSkillBaseDesc = table.description || `ORM operations for ${table.name} records`; + const ormSkillSpecialDesc = ormSkillSpecialGroups.length > 0 + ? ormSkillBaseDesc + '\n\n' + + ormSkillSpecialGroups.map((g) => `**${g.label}:** ${g.fields.map((f) => `\`${f.name}\``).join(', ')}\n${g.description}`).join('\n\n') + : ormSkillBaseDesc; + files.push({ fileName: `${skillName}/references/${refName}.md`, content: buildSkillReference({ title: singularName, - description: table.description || `ORM operations for ${table.name} records`, + description: ormSkillSpecialDesc, language: 'typescript', usage: [ `db.${modelName}.findMany({ select: { id: true } }).execute()`, From 0f235bc581771647f8c45f2f6212f58ec7b862fe Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 15 Mar 2026 20:49:30 +0000 Subject: [PATCH 4/6] =?UTF-8?q?test:=20update=20schema=20snapshot=20?= =?UTF-8?q?=E2=80=94=20trgm=20fields=20removed=20from=20tables=20without?= =?UTF-8?q?=20intentional=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schema-snapshot.test.ts.snap | 200 ------------------ 1 file changed, 200 deletions(-) diff --git a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap index b0d8fa9e7..b70e8d045 100644 --- a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap +++ b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap @@ -300,31 +300,6 @@ type Post { """The method to use when ordering \`Comment\`.""" orderBy: [CommentOrderBy!] = [PRIMARY_KEY_ASC] ): CommentConnection! - - """ - TRGM similarity when searching \`title\`. Returns null when no trgm search filter is active. - """ - titleTrgmSimilarity: Float - - """ - TRGM similarity when searching \`slug\`. Returns null when no trgm search filter is active. - """ - slugTrgmSimilarity: Float - - """ - TRGM similarity when searching \`content\`. Returns null when no trgm search filter is active. - """ - contentTrgmSimilarity: Float - - """ - TRGM similarity when searching \`excerpt\`. Returns null when no trgm search filter is active. - """ - excerptTrgmSimilarity: Float - - """ - Composite search relevance score (0..1, higher = more relevant). Computed by normalizing and averaging all active search signals. Returns null when no search filters are active. - """ - searchScore: Float } """A connection to a list of \`Tag\` values, with data from \`PostTag\`.""" @@ -409,31 +384,6 @@ type Tag { """The method to use when ordering \`PostTag\`.""" orderBy: [PostTagOrderBy!] = [PRIMARY_KEY_ASC] ): PostTagConnection! - - """ - TRGM similarity when searching \`name\`. Returns null when no trgm search filter is active. - """ - nameTrgmSimilarity: Float - - """ - TRGM similarity when searching \`slug\`. Returns null when no trgm search filter is active. - """ - slugTrgmSimilarity: Float - - """ - TRGM similarity when searching \`description\`. Returns null when no trgm search filter is active. - """ - descriptionTrgmSimilarity: Float - - """ - TRGM similarity when searching \`color\`. Returns null when no trgm search filter is active. - """ - colorTrgmSimilarity: Float - - """ - Composite search relevance score (0..1, higher = more relevant). Computed by normalizing and averaging all active search signals. Returns null when no search filters are active. - """ - searchScore: Float } """A connection to a list of \`Post\` values, with data from \`PostTag\`.""" @@ -542,26 +492,6 @@ input PostFilter { """\`comments\` exist.""" commentsExist: Boolean - - """TRGM search on the \`title\` column.""" - trgmTitle: TrgmSearchInput - - """TRGM search on the \`slug\` column.""" - trgmSlug: TrgmSearchInput - - """TRGM search on the \`content\` column.""" - trgmContent: TrgmSearchInput - - """TRGM search on the \`excerpt\` column.""" - trgmExcerpt: TrgmSearchInput - - """ - Composite full-text search. Provide a search string and it will be dispatched - to all text-compatible search algorithms (tsvector, BM25, pg_trgm) - simultaneously. Rows matching ANY algorithm are returned. All matching score - fields are populated. - """ - fullTextSearch: String } """ @@ -938,29 +868,6 @@ input UserFilter { """\`authoredComments\` exist.""" authoredCommentsExist: Boolean - - """TRGM search on the \`email\` column.""" - trgmEmail: TrgmSearchInput - - """TRGM search on the \`username\` column.""" - trgmUsername: TrgmSearchInput - - """TRGM search on the \`display_name\` column.""" - trgmDisplayName: TrgmSearchInput - - """TRGM search on the \`bio\` column.""" - trgmBio: TrgmSearchInput - - """TRGM search on the \`role\` column.""" - trgmRole: TrgmSearchInput - - """ - Composite full-text search. Provide a search string and it will be dispatched - to all text-compatible search algorithms (tsvector, BM25, pg_trgm) - simultaneously. Rows matching ANY algorithm are returned. All matching score - fields are populated. - """ - fullTextSearch: String } """ @@ -1048,17 +955,6 @@ input CommentFilter { """\`childComments\` exist.""" childCommentsExist: Boolean - - """TRGM search on the \`content\` column.""" - trgmContent: TrgmSearchInput - - """ - Composite full-text search. Provide a search string and it will be dispatched - to all text-compatible search algorithms (tsvector, BM25, pg_trgm) - simultaneously. Rows matching ANY algorithm are returned. All matching score - fields are populated. - """ - fullTextSearch: String } """ @@ -1157,26 +1053,6 @@ input TagFilter { """\`postTags\` exist.""" postTagsExist: Boolean - - """TRGM search on the \`name\` column.""" - trgmName: TrgmSearchInput - - """TRGM search on the \`slug\` column.""" - trgmSlug: TrgmSearchInput - - """TRGM search on the \`description\` column.""" - trgmDescription: TrgmSearchInput - - """TRGM search on the \`color\` column.""" - trgmColor: TrgmSearchInput - - """ - Composite full-text search. Provide a search string and it will be dispatched - to all text-compatible search algorithms (tsvector, BM25, pg_trgm) - simultaneously. Rows matching ANY algorithm are returned. All matching score - fields are populated. - """ - fullTextSearch: String } """ @@ -1222,16 +1098,6 @@ enum PostOrderBy { PUBLISHED_AT_DESC CREATED_AT_ASC CREATED_AT_DESC - TITLE_TRGM_SIMILARITY_ASC - TITLE_TRGM_SIMILARITY_DESC - SLUG_TRGM_SIMILARITY_ASC - SLUG_TRGM_SIMILARITY_DESC - CONTENT_TRGM_SIMILARITY_ASC - CONTENT_TRGM_SIMILARITY_DESC - EXCERPT_TRGM_SIMILARITY_ASC - EXCERPT_TRGM_SIMILARITY_DESC - SEARCH_SCORE_ASC - SEARCH_SCORE_DESC } """Methods to use when ordering \`PostTag\`.""" @@ -1269,16 +1135,6 @@ enum TagOrderBy { NAME_DESC SLUG_ASC SLUG_DESC - NAME_TRGM_SIMILARITY_ASC - NAME_TRGM_SIMILARITY_DESC - SLUG_TRGM_SIMILARITY_ASC - SLUG_TRGM_SIMILARITY_DESC - DESCRIPTION_TRGM_SIMILARITY_ASC - DESCRIPTION_TRGM_SIMILARITY_DESC - COLOR_TRGM_SIMILARITY_ASC - COLOR_TRGM_SIMILARITY_DESC - SEARCH_SCORE_ASC - SEARCH_SCORE_DESC } type User { @@ -1349,36 +1205,6 @@ type User { """The method to use when ordering \`Comment\`.""" orderBy: [CommentOrderBy!] = [PRIMARY_KEY_ASC] ): CommentConnection! - - """ - TRGM similarity when searching \`email\`. Returns null when no trgm search filter is active. - """ - emailTrgmSimilarity: Float - - """ - TRGM similarity when searching \`username\`. Returns null when no trgm search filter is active. - """ - usernameTrgmSimilarity: Float - - """ - TRGM similarity when searching \`displayName\`. Returns null when no trgm search filter is active. - """ - displayNameTrgmSimilarity: Float - - """ - TRGM similarity when searching \`bio\`. Returns null when no trgm search filter is active. - """ - bioTrgmSimilarity: Float - - """ - TRGM similarity when searching \`role\`. Returns null when no trgm search filter is active. - """ - roleTrgmSimilarity: Float - - """ - Composite search relevance score (0..1, higher = more relevant). Computed by normalizing and averaging all active search signals. Returns null when no search filters are active. - """ - searchScore: Float } """A connection to a list of \`Post\` values.""" @@ -1472,16 +1298,6 @@ type Comment { """The method to use when ordering \`Comment\`.""" orderBy: [CommentOrderBy!] = [PRIMARY_KEY_ASC] ): CommentConnection! - - """ - TRGM similarity when searching \`content\`. Returns null when no trgm search filter is active. - """ - contentTrgmSimilarity: Float - - """ - Composite search relevance score (0..1, higher = more relevant). Computed by normalizing and averaging all active search signals. Returns null when no search filters are active. - """ - searchScore: Float } """Methods to use when ordering \`Comment\`.""" @@ -1499,10 +1315,6 @@ enum CommentOrderBy { PARENT_ID_DESC CREATED_AT_ASC CREATED_AT_DESC - CONTENT_TRGM_SIMILARITY_ASC - CONTENT_TRGM_SIMILARITY_DESC - SEARCH_SCORE_ASC - SEARCH_SCORE_DESC } """A \`Comment\` edge in the connection.""" @@ -1588,18 +1400,6 @@ enum UserOrderBy { USERNAME_DESC CREATED_AT_ASC CREATED_AT_DESC - EMAIL_TRGM_SIMILARITY_ASC - EMAIL_TRGM_SIMILARITY_DESC - USERNAME_TRGM_SIMILARITY_ASC - USERNAME_TRGM_SIMILARITY_DESC - DISPLAY_NAME_TRGM_SIMILARITY_ASC - DISPLAY_NAME_TRGM_SIMILARITY_DESC - BIO_TRGM_SIMILARITY_ASC - BIO_TRGM_SIMILARITY_DESC - ROLE_TRGM_SIMILARITY_ASC - ROLE_TRGM_SIMILARITY_DESC - SEARCH_SCORE_ASC - SEARCH_SCORE_DESC } """Root meta schema type""" From edfb34eddf337e4f6343671096c4c65e68efce30 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 15 Mar 2026 22:07:59 +0000 Subject: [PATCH 5/6] refactor: make generated AGENTS.md files thin routers instead of full inline references AGENTS.md files are auto-ingested by AI coding agents. Large files reduce task success rates and increase inference cost (ETH Zurich research). Changes: - CLI AGENTS.md: stack overview, quick start, pointer to README.md - Multi-target CLI AGENTS.md: same pattern with target info - ORM AGENTS.md: stack overview, quick start, pointer to README.md - Hooks AGENTS.md: stack overview, quick start, pointer to README.md - Full API references remain in README.md (opt-in, not auto-ingested) - Skills references removed from AGENTS.md (generated separately) --- .../src/core/codegen/cli/docs-generator.ts | 545 ++---------------- .../src/core/codegen/hooks-docs-generator.ts | 174 +----- .../src/core/codegen/orm/docs-generator.ts | 150 +---- 3 files changed, 89 insertions(+), 780 deletions(-) diff --git a/graphql/codegen/src/core/codegen/cli/docs-generator.ts b/graphql/codegen/src/core/codegen/cli/docs-generator.ts index 685ad5439..ea2c4eb6d 100644 --- a/graphql/codegen/src/core/codegen/cli/docs-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/docs-generator.ts @@ -9,7 +9,6 @@ import { getSearchFields, categorizeSpecialFields, buildSpecialFieldsMarkdown, - buildSpecialFieldsPlain, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, @@ -213,252 +212,49 @@ export function generateAgentsDocs( tables: CleanTable[], customOperations: CleanOperation[], toolName: string, - registry?: TypeRegistry, + _registry?: TypeRegistry, ): GeneratedDocFile { const lines: string[] = []; + const tableCount = tables.length; + const customOpCount = customOperations.length; - lines.push(`# ${toolName} CLI - Agent Reference`); + lines.push(`# ${toolName} CLI`); lines.push(''); lines.push(''); - lines.push('> This document is structured for LLM/agent consumption.'); - lines.push(''); - - lines.push('## OVERVIEW'); - lines.push(''); - lines.push(`\`${toolName}\` is a CLI tool for interacting with a GraphQL API.`); - lines.push('All commands output JSON to stdout. All commands accept `--help` or `-h` for usage.'); - lines.push(`Configuration is stored at \`~/.${toolName}/config/\` via appstash.`); - lines.push(''); - - lines.push('## PREREQUISITES'); - lines.push(''); - lines.push('Before running any data commands, you must:'); - lines.push(''); - lines.push(`1. Create a context: \`${toolName} context create --endpoint \``); - lines.push(`2. Activate it: \`${toolName} context use \``); - lines.push(`3. Authenticate: \`${toolName} auth set-token \``); - lines.push(''); - - lines.push('## TOOLS'); lines.push(''); - lines.push('### TOOL: context'); - lines.push(''); - lines.push('Manage named API endpoint contexts (like kubectl contexts).'); - lines.push(''); - lines.push('```'); - lines.push('SUBCOMMANDS:'); - lines.push(` ${toolName} context create --endpoint Create a new context`); - lines.push(` ${toolName} context list List all contexts`); - lines.push(` ${toolName} context use Set active context`); - lines.push(` ${toolName} context current Show active context`); - lines.push(` ${toolName} context delete Delete a context`); - lines.push(''); - lines.push('INPUT:'); - lines.push(' name: string (required) - Context identifier'); - lines.push(' endpoint: string (required for create) - GraphQL endpoint URL'); - lines.push(''); - lines.push('OUTPUT: JSON'); - lines.push(' create: { name, endpoint }'); - lines.push(' list: [{ name, endpoint, isCurrent, hasCredentials }]'); - lines.push(' use: { name, endpoint }'); - lines.push(' current: { name, endpoint }'); - lines.push(' delete: { deleted: name }'); - lines.push('```'); + lines.push('## Stack'); lines.push(''); - - lines.push('### TOOL: auth'); - lines.push(''); - lines.push('Manage authentication tokens per context.'); - lines.push(''); - lines.push('```'); - lines.push('SUBCOMMANDS:'); - lines.push(` ${toolName} auth set-token Store bearer token for current context`); - lines.push(` ${toolName} auth status Show auth status for all contexts`); - lines.push(` ${toolName} auth logout Remove credentials for current context`); - lines.push(''); - lines.push('INPUT:'); - lines.push(' token: string (required for set-token) - Bearer token value'); - lines.push(''); - lines.push('OUTPUT: JSON'); - lines.push(' set-token: { context, status: "authenticated" }'); - lines.push(' status: [{ context, authenticated: boolean }]'); - lines.push(' logout: { context, status: "logged out" }'); - lines.push('```'); - lines.push(''); - - lines.push('### TOOL: config'); - lines.push(''); - lines.push('Manage per-context key-value configuration variables.'); - lines.push(''); - lines.push('```'); - lines.push('SUBCOMMANDS:'); - lines.push(` ${toolName} config get Get a config value`); - lines.push(` ${toolName} config set Set a config value`); - lines.push(` ${toolName} config list List all config values`); - lines.push(` ${toolName} config delete Delete a config value`); - lines.push(''); - lines.push('INPUT:'); - lines.push(' key: string (required for get/set/delete) - Variable name'); - lines.push(' value: string (required for set) - Variable value'); - lines.push(''); - lines.push('OUTPUT: JSON'); - lines.push(' get: { key, value }'); - lines.push(' set: { key, value }'); - lines.push(' list: { vars: { key: value, ... } }'); - lines.push(' delete: { deleted: key }'); - lines.push('```'); + lines.push(`- Generated CLI for a GraphQL API (TypeScript)`); + lines.push(`- ${tableCount} table${tableCount !== 1 ? 's' : ''}${customOpCount > 0 ? `, ${customOpCount} custom operation${customOpCount !== 1 ? 's' : ''}` : ''}`); + lines.push(`- Config stored at \`~/.${toolName}/config/\` via appstash`); lines.push(''); - for (const table of tables) { - const { singularName } = getTableNames(table); - const kebab = toKebabCase(singularName); - const pk = getPrimaryKeyInfo(table)[0]; - const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table, registry); - const defaultFields = getFieldsWithDefaults(table, registry); - const requiredCreateFields = editableFields.filter((f) => !defaultFields.has(f.name)); - const optionalCreateFields = editableFields.filter((f) => defaultFields.has(f.name)); - const createFlags = [ - ...requiredCreateFields.map((f) => `--${f.name} `), - ...optionalCreateFields.map((f) => `[--${f.name} ]`), - ].join(' '); - - lines.push(`### TOOL: ${kebab}`); - lines.push(''); - lines.push(`CRUD operations for ${table.name} records.`); - lines.push(''); - lines.push('```'); - lines.push('SUBCOMMANDS:'); - lines.push(` ${toolName} ${kebab} list List all records`); - lines.push(` ${toolName} ${kebab} get --${pk.name} Get one record`); - lines.push(` ${toolName} ${kebab} create ${createFlags}`); - lines.push(` ${toolName} ${kebab} update --${pk.name} ${editableFields.map((f) => `[--${f.name} ]`).join(' ')}`); - lines.push(` ${toolName} ${kebab} delete --${pk.name} Delete one record`); - lines.push(''); - lines.push('INPUT FIELDS:'); - for (const f of scalarFields) { - const isPk = f.name === pk.name; - lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}${isPk ? ' (primary key)' : ''}`); - } - lines.push(''); - lines.push('EDITABLE FIELDS (for create/update):'); - for (const f of editableFields) { - const optLabel = defaultFields.has(f.name) ? ' (optional, has backend default)' : ''; - lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}${optLabel}`); - } - lines.push(''); - const agentSpecialGroups = categorizeSpecialFields(table, registry); - const agentSpecialLines = buildSpecialFieldsPlain(agentSpecialGroups); - if (agentSpecialLines.length > 0) { - for (const sl of agentSpecialLines) { - lines.push(sl); - } - lines.push(''); - } - lines.push('OUTPUT: JSON'); - lines.push(` list: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`); - lines.push(` get: { ${scalarFields.map((f) => f.name).join(', ')} }`); - lines.push(` create: { ${scalarFields.map((f) => f.name).join(', ')} }`); - lines.push(` update: { ${scalarFields.map((f) => f.name).join(', ')} }`); - lines.push(` delete: { ${pk.name} }`); - lines.push('```'); - lines.push(''); - } - - for (const op of customOperations) { - const kebab = toKebabCase(op.name); - const flat = flattenArgs(op.args, registry); - - lines.push(`### TOOL: ${kebab}`); - lines.push(''); - lines.push(op.description || op.name); - lines.push(''); - lines.push('```'); - lines.push(`TYPE: ${op.kind}`); - if (flat.length > 0) { - const flags = flattenedArgsToFlags(flat); - lines.push(`USAGE: ${toolName} ${kebab} ${flags}`); - lines.push(''); - lines.push('INPUT:'); - for (const a of flat) { - const reqLabel = a.required ? ' (required)' : ''; - lines.push(` ${a.flag}: ${a.type}${reqLabel}`); - } - } else { - lines.push(`USAGE: ${toolName} ${kebab}`); - lines.push(''); - lines.push('INPUT: none'); - } - lines.push(''); - lines.push('OUTPUT: JSON'); - lines.push('```'); - lines.push(''); - } - - lines.push('## WORKFLOWS'); - lines.push(''); - lines.push('### Initial setup'); + lines.push('## Quick Start'); lines.push(''); lines.push('```bash'); - lines.push(`${toolName} context create dev --endpoint http://localhost:5000/graphql`); + lines.push(`${toolName} context create dev --endpoint `); lines.push(`${toolName} context use dev`); - lines.push(`${toolName} auth set-token eyJhbGciOiJIUzI1NiIs...`); + lines.push(`${toolName} auth set-token `); lines.push('```'); lines.push(''); - if (tables.length > 0) { - const firstTable = tables[0]; - const { singularName } = getTableNames(firstTable); - const kebab = toKebabCase(singularName); - const editableFields = getEditableFields(firstTable, registry); - const pk = getPrimaryKeyInfo(firstTable)[0]; - - lines.push(`### CRUD workflow (${kebab})`); - lines.push(''); - lines.push('```bash'); - lines.push(`# List all`); - lines.push(`${toolName} ${kebab} list`); - lines.push(''); - lines.push(`# Create`); - lines.push(`${toolName} ${kebab} create ${editableFields.map((f) => `--${f.name} "value"`).join(' ')}`); - lines.push(''); - lines.push(`# Get by ${pk.name}`); - lines.push(`${toolName} ${kebab} get --${pk.name} `); - lines.push(''); - lines.push(`# Update`); - lines.push(`${toolName} ${kebab} update --${pk.name} --${editableFields[0]?.name || 'field'} "new-value"`); - lines.push(''); - lines.push(`# Delete`); - lines.push(`${toolName} ${kebab} delete --${pk.name} `); - lines.push('```'); - lines.push(''); - } - - lines.push('### Piping output'); + lines.push('## Resources'); lines.push(''); - lines.push('```bash'); - lines.push(`# Pretty print`); - lines.push(`${toolName} car list | jq '.'`); + lines.push(`- **Full API reference:** [README.md](./README.md) — CRUD docs for all ${tableCount} tables`); + lines.push('- **Schema types:** [types.ts](./types.ts)'); lines.push(''); - lines.push(`# Extract field`); - lines.push(`${toolName} car list | jq '.[].id'`); + + lines.push('## Conventions'); lines.push(''); - lines.push(`# Count results`); - lines.push(`${toolName} car list | jq 'length'`); - lines.push('```'); + lines.push('- All commands output JSON to stdout'); + lines.push('- Use `--help` on any command for usage'); + lines.push('- Exit 0 = success, 1 = error'); lines.push(''); - lines.push('## ERROR HANDLING'); - lines.push(''); - lines.push('All errors are written to stderr. Exit codes:'); - lines.push('- `0`: Success'); - lines.push('- `1`: Error (auth failure, not found, validation error, network error)'); + lines.push('## Boundaries'); lines.push(''); - lines.push('Common errors:'); - lines.push('- "No active context": Run `context use ` first'); - lines.push('- "Not authenticated": Run `auth set-token ` first'); - lines.push('- "Record not found": The requested ID does not exist'); + lines.push('All files in this directory are generated. Do not edit manually.'); lines.push(''); return { @@ -1240,308 +1036,57 @@ export function generateMultiTargetReadme( export function generateMultiTargetAgentsDocs( input: MultiTargetDocsInput, ): GeneratedDocFile { - const { toolName, builtinNames, targets, registry } = input; + const { toolName, builtinNames, targets } = input; const lines: string[] = []; + const totalTables = targets.reduce((sum, t) => sum + t.tables.length, 0); + const totalCustomOps = targets.reduce((sum, t) => sum + t.customOperations.length, 0); - lines.push(`# ${toolName} CLI - Agent Reference`); + lines.push(`# ${toolName} CLI`); lines.push(''); lines.push(''); - lines.push('> This document is structured for LLM/agent consumption.'); lines.push(''); - lines.push('## OVERVIEW'); - lines.push(''); - lines.push(`\`${toolName}\` is a unified multi-target CLI for interacting with multiple GraphQL APIs.`); - lines.push('All commands output JSON to stdout. All commands accept `--help` or `-h` for usage.'); - lines.push(`Configuration is stored at \`~/.${toolName}/config/\` via appstash.`); - lines.push(''); - lines.push('TARGETS:'); - for (const tgt of targets) { - lines.push(` ${tgt.name}: ${tgt.endpoint}`); - } + lines.push('## Stack'); lines.push(''); - lines.push('COMMAND FORMAT:'); - lines.push(` ${toolName} : [flags] Target-specific commands`); - lines.push(` ${toolName} ${builtinNames.context} [flags] Context management`); - lines.push(` ${toolName} ${builtinNames.auth} [flags] Authentication`); - lines.push(` ${toolName} ${builtinNames.config} [flags] Config key-value store`); + lines.push(`- Unified multi-target CLI for GraphQL APIs (TypeScript)`); + lines.push(`- ${targets.length} target${targets.length !== 1 ? 's' : ''}: ${targets.map((t) => t.name).join(', ')}`); + lines.push(`- ${totalTables} table${totalTables !== 1 ? 's' : ''}${totalCustomOps > 0 ? `, ${totalCustomOps} custom operation${totalCustomOps !== 1 ? 's' : ''}` : ''}`); + lines.push(`- Config stored at \`~/.${toolName}/config/\` via appstash`); lines.push(''); - lines.push('## PREREQUISITES'); - lines.push(''); - lines.push('Before running any data commands, you must:'); - lines.push(''); - lines.push(`1. Create a context: \`${toolName} ${builtinNames.context} create \``); - lines.push(` (prompts for per-target endpoints, defaults baked from config)`); - lines.push(`2. Activate it: \`${toolName} ${builtinNames.context} use \``); - lines.push(`3. Authenticate: \`${toolName} ${builtinNames.auth} set-token \``); - lines.push(''); - lines.push('For local development, create a context accepting all defaults:'); + lines.push('## Quick Start'); lines.push(''); lines.push('```bash'); - lines.push(`${toolName} ${builtinNames.context} create local`); - lines.push(`${toolName} ${builtinNames.context} use local`); + lines.push(`${toolName} ${builtinNames.context} create dev`); + lines.push(`${toolName} ${builtinNames.context} use dev`); lines.push(`${toolName} ${builtinNames.auth} set-token `); lines.push('```'); lines.push(''); - lines.push('## TOOLS'); - lines.push(''); - - lines.push(`### TOOL: ${builtinNames.context}`); - lines.push(''); - lines.push('Manage named API endpoint contexts. Each context stores per-target endpoint overrides.'); + lines.push('## Command Format'); lines.push(''); lines.push('```'); - lines.push('SUBCOMMANDS:'); - lines.push(` ${toolName} ${builtinNames.context} create Create a new context`); - lines.push(` ${toolName} ${builtinNames.context} list List all contexts`); - lines.push(` ${toolName} ${builtinNames.context} use Set active context`); - lines.push(` ${toolName} ${builtinNames.context} current Show active context`); - lines.push(` ${toolName} ${builtinNames.context} delete Delete a context`); - lines.push(''); - lines.push('CREATE OPTIONS:'); - for (const tgt of targets) { - lines.push(` --${tgt.name}-endpoint: string (default: ${tgt.endpoint})`); - } - lines.push(''); - lines.push('OUTPUT: JSON'); - lines.push(' create: { name, endpoint, targets }'); - lines.push(' list: [{ name, endpoint, isCurrent, hasCredentials }]'); - lines.push(' use: { name, endpoint }'); - lines.push(' current: { name, endpoint }'); - lines.push(' delete: { deleted: name }'); + lines.push(`${toolName} : [flags]`); lines.push('```'); lines.push(''); - lines.push(`### TOOL: ${builtinNames.auth}`); + lines.push('## Resources'); lines.push(''); - lines.push('Manage authentication tokens per context. One shared token across all targets.'); - lines.push(''); - lines.push('```'); - lines.push('SUBCOMMANDS:'); - lines.push(` ${toolName} ${builtinNames.auth} set-token Store bearer token for current context`); - lines.push(` ${toolName} ${builtinNames.auth} status Show auth status for all contexts`); - lines.push(` ${toolName} ${builtinNames.auth} logout Remove credentials for current context`); - lines.push(''); - lines.push('INPUT:'); - lines.push(' token: string (required for set-token) - Bearer token value'); - lines.push(''); - lines.push('OUTPUT: JSON'); - lines.push(' set-token: { context, status: "authenticated" }'); - lines.push(' status: [{ context, authenticated: boolean }]'); - lines.push(' logout: { context, status: "logged out" }'); - lines.push('```'); + lines.push(`- **Full API reference:** [README.md](./README.md) — CRUD docs for all ${totalTables} tables across ${targets.length} targets`); + lines.push('- **Schema types:** [types.ts](./types.ts)'); + lines.push('- **SDK helpers:** [helpers.ts](./helpers.ts) — typed client factories'); lines.push(''); - lines.push(`### TOOL: ${builtinNames.config}`); - lines.push(''); - lines.push('Manage per-context key-value configuration variables.'); + lines.push('## Conventions'); lines.push(''); - lines.push('```'); - lines.push('SUBCOMMANDS:'); - lines.push(` ${toolName} ${builtinNames.config} get Get a config value`); - lines.push(` ${toolName} ${builtinNames.config} set Set a config value`); - lines.push(` ${toolName} ${builtinNames.config} list List all config values`); - lines.push(` ${toolName} ${builtinNames.config} delete Delete a config value`); - lines.push(''); - lines.push('INPUT:'); - lines.push(' key: string (required for get/set/delete) - Variable name'); - lines.push(' value: string (required for set) - Variable value'); - lines.push(''); - lines.push('OUTPUT: JSON'); - lines.push(' get: { key, value }'); - lines.push(' set: { key, value }'); - lines.push(' list: { vars: { key: value, ... } }'); - lines.push(' delete: { deleted: key }'); - lines.push('```'); + lines.push('- All commands output JSON to stdout'); + lines.push('- Use `--help` on any command for usage'); + lines.push('- Exit 0 = success, 1 = error'); lines.push(''); - lines.push('### TOOL: helpers (SDK)'); - lines.push(''); - lines.push('Typed client factories for use in scripts and services (generated helpers.ts).'); - lines.push('Resolves credentials via: appstash store -> env vars -> throw.'); - lines.push(''); - lines.push('```'); - lines.push('FACTORIES:'); - for (const tgt of targets) { - const pascalName = tgt.name.charAt(0).toUpperCase() + tgt.name.slice(1); - lines.push(` create${pascalName}Client(contextName?) Create a configured ${tgt.name} ORM client`); - } - lines.push(''); - lines.push('USAGE:'); - lines.push(` import { create${targets[0] ? targets[0].name.charAt(0).toUpperCase() + targets[0].name.slice(1) : 'Target'}Client } from './helpers';`); - lines.push(` const client = create${targets[0] ? targets[0].name.charAt(0).toUpperCase() + targets[0].name.slice(1) : 'Target'}Client();`); - lines.push(''); - lines.push('CREDENTIAL RESOLUTION:'); - lines.push(` 1. appstash store (~/.${toolName}/config/)`); - const envPrefix = toolName.toUpperCase().replace(/-/g, '_'); - lines.push(` 2. env vars (${envPrefix}_TOKEN, ${envPrefix}__ENDPOINT)`); - lines.push(' 3. throws with actionable error message'); - lines.push('```'); - lines.push(''); - - for (const tgt of targets) { - for (const table of tgt.tables) { - const { singularName } = getTableNames(table); - const kebab = toKebabCase(singularName); - const pk = getPrimaryKeyInfo(table)[0]; - const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table, registry); - const defaultFields = getFieldsWithDefaults(table, registry); - const requiredCreateFields = editableFields.filter((f) => !defaultFields.has(f.name)); - const optionalCreateFields = editableFields.filter((f) => defaultFields.has(f.name)); - const createFlags = [ - ...requiredCreateFields.map((f) => `--${f.name} `), - ...optionalCreateFields.map((f) => `[--${f.name} ]`), - ].join(' '); - - lines.push(`### TOOL: ${tgt.name}:${kebab}`); - lines.push(''); - lines.push(`CRUD operations for ${table.name} records (${tgt.name} target).`); - lines.push(''); - lines.push('```'); - lines.push('SUBCOMMANDS:'); - lines.push(` ${toolName} ${tgt.name}:${kebab} list List all records`); - lines.push(` ${toolName} ${tgt.name}:${kebab} get --${pk.name} Get one record`); - lines.push(` ${toolName} ${tgt.name}:${kebab} create ${createFlags}`); - lines.push(` ${toolName} ${tgt.name}:${kebab} update --${pk.name} ${editableFields.map((f) => `[--${f.name} ]`).join(' ')}`); - lines.push(` ${toolName} ${tgt.name}:${kebab} delete --${pk.name} Delete one record`); - lines.push(''); - lines.push('INPUT FIELDS:'); - for (const f of scalarFields) { - const isPk = f.name === pk.name; - lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}${isPk ? ' (primary key)' : ''}`); - } - lines.push(''); - lines.push('EDITABLE FIELDS (for create/update):'); - for (const f of editableFields) { - const optLabel = defaultFields.has(f.name) ? ' (optional, has backend default)' : ''; - lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}${optLabel}`); - } - lines.push(''); - const mtAgentSpecialGroups = categorizeSpecialFields(table, registry); - const mtAgentSpecialLines = buildSpecialFieldsPlain(mtAgentSpecialGroups); - if (mtAgentSpecialLines.length > 0) { - for (const sl of mtAgentSpecialLines) { - lines.push(sl); - } - lines.push(''); - } - lines.push('OUTPUT: JSON'); - lines.push(` list: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`); - lines.push(` get: { ${scalarFields.map((f) => f.name).join(', ')} }`); - lines.push(` create: { ${scalarFields.map((f) => f.name).join(', ')} }`); - lines.push(` update: { ${scalarFields.map((f) => f.name).join(', ')} }`); - lines.push(` delete: { ${pk.name} }`); - lines.push('```'); - lines.push(''); - } - - for (const op of tgt.customOperations) { - const kebab = toKebabCase(op.name); - const flat = flattenArgs(op.args, registry); - - lines.push(`### TOOL: ${tgt.name}:${kebab}`); - lines.push(''); - lines.push(op.description || op.name); - lines.push(''); - lines.push('```'); - lines.push(`TYPE: ${op.kind}`); - if (flat.length > 0) { - const flags = flattenedArgsToFlags(flat); - lines.push(`USAGE: ${toolName} ${tgt.name}:${kebab} ${flags}`); - lines.push(''); - lines.push('INPUT:'); - for (const a of flat) { - const reqLabel = a.required ? ' (required)' : ''; - lines.push(` ${a.flag}: ${a.type}${reqLabel}`); - } - } else { - lines.push(`USAGE: ${toolName} ${tgt.name}:${kebab}`); - lines.push(''); - lines.push('INPUT: none'); - } - if (tgt.isAuthTarget && op.kind === 'mutation') { - lines.push(''); - lines.push('FLAGS:'); - lines.push(' --save-token: boolean - Auto-save returned token to credentials'); - } - lines.push(''); - lines.push('OUTPUT: JSON'); - lines.push('```'); - lines.push(''); - } - } - - lines.push('## WORKFLOWS'); - lines.push(''); - lines.push('### Initial setup'); - lines.push(''); - lines.push('```bash'); - lines.push(`${toolName} ${builtinNames.context} create dev`); - lines.push(`${toolName} ${builtinNames.context} use dev`); - lines.push(`${toolName} ${builtinNames.auth} set-token eyJhbGciOiJIUzI1NiIs...`); - lines.push('```'); - lines.push(''); - - lines.push('### Switch environment'); - lines.push(''); - lines.push('```bash'); - lines.push(`${toolName} ${builtinNames.context} create production \\`); - for (let i = 0; i < targets.length; i++) { - const tgt = targets[i]; - const continuation = i < targets.length - 1 ? ' \\' : ''; - lines.push(` --${tgt.name}-endpoint https://${tgt.name}.prod.example.com/graphql${continuation}`); - } - lines.push(`${toolName} ${builtinNames.context} use production`); - lines.push('```'); - lines.push(''); - - if (targets.length > 0 && targets[0].tables.length > 0) { - const tgt = targets[0]; - const table = tgt.tables[0]; - const { singularName } = getTableNames(table); - const kebab = toKebabCase(singularName); - const editableFields = getEditableFields(table, registry); - const pk = getPrimaryKeyInfo(table)[0]; - - lines.push(`### CRUD workflow (${tgt.name}:${kebab})`); - lines.push(''); - lines.push('```bash'); - lines.push(`${toolName} ${tgt.name}:${kebab} list`); - lines.push(`${toolName} ${tgt.name}:${kebab} create ${editableFields.map((f) => `--${f.name} "value"`).join(' ')}`); - lines.push(`${toolName} ${tgt.name}:${kebab} get --${pk.name} `); - lines.push(`${toolName} ${tgt.name}:${kebab} update --${pk.name} --${editableFields[0]?.name || 'field'} "new-value"`); - lines.push(`${toolName} ${tgt.name}:${kebab} delete --${pk.name} `); - lines.push('```'); - lines.push(''); - } - - lines.push('### Piping output'); - lines.push(''); - lines.push('```bash'); - if (targets.length > 0 && targets[0].tables.length > 0) { - const tgt = targets[0]; - const kebab = toKebabCase(getTableNames(tgt.tables[0]).singularName); - lines.push(`${toolName} ${tgt.name}:${kebab} list | jq '.'`); - lines.push(`${toolName} ${tgt.name}:${kebab} list | jq '.[].id'`); - lines.push(`${toolName} ${tgt.name}:${kebab} list | jq 'length'`); - } - lines.push('```'); - lines.push(''); - - lines.push('## ERROR HANDLING'); - lines.push(''); - lines.push('All errors are written to stderr. Exit codes:'); - lines.push('- `0`: Success'); - lines.push('- `1`: Error (auth failure, not found, validation error, network error)'); + lines.push('## Boundaries'); lines.push(''); - lines.push('Common errors:'); - lines.push(`- "No active context": Run \`${builtinNames.context} use \` first`); - lines.push(`- "Not authenticated": Run \`${builtinNames.auth} set-token \` first`); - lines.push('- "Unknown target": The target name is not recognized'); - lines.push('- "Record not found": The requested ID does not exist'); + lines.push('All files in this directory are generated. Do not edit manually.'); lines.push(''); return { diff --git a/graphql/codegen/src/core/codegen/hooks-docs-generator.ts b/graphql/codegen/src/core/codegen/hooks-docs-generator.ts index 18453f953..4d455973d 100644 --- a/graphql/codegen/src/core/codegen/hooks-docs-generator.ts +++ b/graphql/codegen/src/core/codegen/hooks-docs-generator.ts @@ -184,24 +184,22 @@ export function generateHooksAgentsDocs( customOperations: CleanOperation[], ): GeneratedDocFile { const lines: string[] = []; + const tableCount = tables.length; + const customOpCount = customOperations.length; - lines.push('# React Query Hooks - Agent Reference'); + lines.push('# React Query Hooks'); lines.push(''); lines.push(''); - lines.push('> This document is structured for LLM/agent consumption.'); lines.push(''); - lines.push('## OVERVIEW'); + lines.push('## Stack'); lines.push(''); - lines.push( - 'React Query hooks wrapping ORM operations for data fetching and mutations.', - ); - lines.push( - 'All query hooks return `UseQueryResult`. All mutation hooks return `UseMutationResult`.', - ); + lines.push('- React Query hooks wrapping ORM operations (TypeScript)'); + lines.push(`- ${tableCount} table${tableCount !== 1 ? 's' : ''}${customOpCount > 0 ? `, ${customOpCount} custom operation${customOpCount !== 1 ? 's' : ''}` : ''}`); + lines.push('- Query hooks return `UseQueryResult`, mutation hooks return `UseMutationResult`'); lines.push(''); - lines.push('## SETUP'); + lines.push('## Quick Start'); lines.push(''); lines.push('```typescript'); lines.push("import { configure } from './hooks';"); @@ -213,150 +211,24 @@ export function generateHooksAgentsDocs( lines.push('```'); lines.push(''); - lines.push('## HOOKS'); + lines.push('## Resources'); + lines.push(''); + lines.push(`- **Full API reference:** [README.md](./README.md) — hook docs for all ${tableCount} tables`); + lines.push('- **Schema types:** [types.ts](./types.ts)'); + lines.push('- **Hooks module:** [hooks.ts](./hooks.ts)'); lines.push(''); - for (const table of tables) { - const { singularName, pluralName } = getTableNames(table); - const pk = getPrimaryKeyInfo(table)[0]; - const scalarFields = getScalarFields(table); - - lines.push(`### HOOK: ${getListQueryHookName(table)}`); - lines.push(''); - lines.push(`${table.description || `List all ${pluralName}`}.`); - lines.push(''); - lines.push('```'); - lines.push(`TYPE: query`); - lines.push( - `USAGE: ${getListQueryHookName(table)}({ selection: { fields: { ... } } })`, - ); - lines.push(''); - lines.push('INPUT:'); - lines.push( - ' selection: { fields: Record } - Fields to select', - ); - lines.push(''); - lines.push('OUTPUT: UseQueryResult>'); - lines.push('```'); - lines.push(''); - - if (hasValidPrimaryKey(table)) { - lines.push(`### HOOK: ${getSingleQueryHookName(table)}`); - lines.push(''); - lines.push(`${table.description || `Get a single ${singularName} by ${pk.name}`}.`); - lines.push(''); - lines.push('```'); - lines.push(`TYPE: query`); - lines.push( - `USAGE: ${getSingleQueryHookName(table)}({ ${pk.name}: '', selection: { fields: { ... } } })`, - ); - lines.push(''); - lines.push('INPUT:'); - lines.push(` ${pk.name}: ${pk.tsType} (required)`); - lines.push( - ' selection: { fields: Record } - Fields to select', - ); - lines.push(''); - lines.push('OUTPUT: UseQueryResult<{'); - for (const f of scalarFields) { - lines.push(` ${f.name}: ${fieldTypeToTs(f.type)}`); - } - lines.push('}>'); - lines.push('```'); - lines.push(''); - } - - lines.push(`### HOOK: ${getCreateMutationHookName(table)}`); - lines.push(''); - lines.push(`${table.description || `Create a new ${singularName}`}.`); - lines.push(''); - lines.push('```'); - lines.push('TYPE: mutation'); - lines.push( - `USAGE: const { mutate } = ${getCreateMutationHookName(table)}({ selection: { fields: { ... } } })`, - ); - lines.push(''); - lines.push('OUTPUT: UseMutationResult'); - lines.push('```'); - lines.push(''); - - if (hasValidPrimaryKey(table)) { - lines.push(`### HOOK: ${getUpdateMutationHookName(table)}`); - lines.push(''); - lines.push(`${table.description || `Update an existing ${singularName}`}.`); - lines.push(''); - lines.push('```'); - lines.push('TYPE: mutation'); - lines.push( - `USAGE: const { mutate } = ${getUpdateMutationHookName(table)}({ selection: { fields: { ... } } })`, - ); - lines.push(''); - lines.push('OUTPUT: UseMutationResult'); - lines.push('```'); - lines.push(''); - - lines.push(`### HOOK: ${getDeleteMutationHookName(table)}`); - lines.push(''); - lines.push(`${table.description || `Delete a ${singularName}`}.`); - lines.push(''); - lines.push('```'); - lines.push('TYPE: mutation'); - lines.push( - `USAGE: const { mutate } = ${getDeleteMutationHookName(table)}({})`, - ); - lines.push(''); - lines.push('OUTPUT: UseMutationResult'); - lines.push('```'); - lines.push(''); - } - } - - if (customOperations.length > 0) { - lines.push('## CUSTOM OPERATION HOOKS'); - lines.push(''); - - for (const op of customOperations) { - const hookName = getCustomHookName(op); + lines.push('## Conventions'); + lines.push(''); + lines.push('- Query hooks: `useQuery`, `useQuery`'); + lines.push('- Mutation hooks: `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`'); + lines.push('- All hooks accept a `selection` parameter to pick fields'); + lines.push(''); - lines.push(`### HOOK: ${hookName}`); - lines.push(''); - lines.push(op.description || op.name); - lines.push(''); - lines.push('```'); - lines.push(`TYPE: ${op.kind}`); - if (op.args.length > 0) { - if (op.kind === 'mutation') { - lines.push(`USAGE: const { mutate } = ${hookName}()`); - lines.push( - ` mutate({ ${op.args.map((a) => `${a.name}: `).join(', ')} })`, - ); - } else { - lines.push( - `USAGE: ${hookName}({ ${op.args.map((a) => `${a.name}: `).join(', ')} })`, - ); - } - lines.push(''); - lines.push('INPUT:'); - for (const arg of op.args) { - lines.push(` ${arg.name}: ${formatArgType(arg)}`); - } - } else { - lines.push(`USAGE: ${hookName}()`); - lines.push(''); - lines.push('INPUT: none'); - } - lines.push(''); - lines.push( - `OUTPUT: ${op.kind === 'query' ? 'UseQueryResult' : 'UseMutationResult'}`, - ); - lines.push('```'); - lines.push(''); - } - } + lines.push('## Boundaries'); + lines.push(''); + lines.push('All files in this directory are generated. Do not edit manually.'); + lines.push(''); return { fileName: 'AGENTS.md', diff --git a/graphql/codegen/src/core/codegen/orm/docs-generator.ts b/graphql/codegen/src/core/codegen/orm/docs-generator.ts index 0b6c20469..7e6bfae7a 100644 --- a/graphql/codegen/src/core/codegen/orm/docs-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/docs-generator.ts @@ -8,7 +8,6 @@ import { getEditableFields, categorizeSpecialFields, buildSpecialFieldsMarkdown, - buildSpecialFieldsPlain, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, @@ -162,24 +161,22 @@ export function generateOrmAgentsDocs( customOperations: CleanOperation[], ): GeneratedDocFile { const lines: string[] = []; + const tableCount = tables.length; + const customOpCount = customOperations.length; - lines.push('# ORM Client - Agent Reference'); + lines.push('# ORM Client'); lines.push(''); lines.push(''); - lines.push('> This document is structured for LLM/agent consumption.'); lines.push(''); - lines.push('## OVERVIEW'); + lines.push('## Stack'); lines.push(''); - lines.push( - 'Prisma-like ORM client for interacting with a GraphQL API.', - ); - lines.push( - 'All methods return a query builder. Call `.execute()` to run the query.', - ); + lines.push('- Prisma-like ORM client for a GraphQL API (TypeScript)'); + lines.push(`- ${tableCount} model${tableCount !== 1 ? 's' : ''}${customOpCount > 0 ? `, ${customOpCount} custom operation${customOpCount !== 1 ? 's' : ''}` : ''}`); + lines.push('- All methods return a query builder; call `.execute()` to run'); lines.push(''); - lines.push('## SETUP'); + lines.push('## Quick Start'); lines.push(''); lines.push('```typescript'); lines.push("import { createClient } from './orm';"); @@ -191,129 +188,24 @@ export function generateOrmAgentsDocs( lines.push('```'); lines.push(''); - lines.push('## MODELS'); + lines.push('## Resources'); + lines.push(''); + lines.push(`- **Full API reference:** [README.md](./README.md) — model docs for all ${tableCount} tables`); + lines.push('- **Schema types:** [types.ts](./types.ts)'); + lines.push('- **ORM client:** [orm.ts](./orm.ts)'); lines.push(''); - for (const table of tables) { - const { singularName } = getTableNames(table); - const pk = getPrimaryKeyInfo(table)[0]; - const scalarFields = getScalarFields(table); - const editableFields = getEditableFields(table); - - lines.push(`### MODEL: ${singularName}`); - lines.push(''); - lines.push(`Access: \`db.${singularName}\``); - lines.push(''); - lines.push('```'); - lines.push('METHODS:'); - lines.push( - ` db.${singularName}.findMany({ select, where?, orderBy?, first?, offset? })`, - ); - lines.push( - ` db.${singularName}.findOne({ ${pk.name}, select })`, - ); - lines.push( - ` db.${singularName}.create({ data: { ${editableFields.map((f) => f.name).join(', ')} }, select })`, - ); - lines.push( - ` db.${singularName}.update({ where: { ${pk.name} }, data, select })`, - ); - lines.push(` db.${singularName}.delete({ where: { ${pk.name} } })`); - lines.push(''); - lines.push('FIELDS:'); - for (const f of scalarFields) { - const isPk = f.name === pk.name; - lines.push( - ` ${f.name}: ${fieldTypeToTs(f.type)}${isPk ? ' (primary key)' : ''}`, - ); - } - lines.push(''); - lines.push('EDITABLE FIELDS:'); - for (const f of editableFields) { - lines.push(` ${f.name}: ${fieldTypeToTs(f.type)}`); - } - lines.push(''); - const ormAgentSpecialGroups = categorizeSpecialFields(table); - const ormAgentSpecialLines = buildSpecialFieldsPlain(ormAgentSpecialGroups); - if (ormAgentSpecialLines.length > 0) { - for (const sl of ormAgentSpecialLines) { - lines.push(sl); - } - lines.push(''); - } - lines.push('OUTPUT: Promise'); - lines.push( - ` findMany: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`, - ); - lines.push( - ` findOne: { ${scalarFields.map((f) => f.name).join(', ')} }`, - ); - lines.push( - ` create: { ${scalarFields.map((f) => f.name).join(', ')} }`, - ); - lines.push( - ` update: { ${scalarFields.map((f) => f.name).join(', ')} }`, - ); - lines.push(` delete: { ${pk.name} }`); - lines.push('```'); - lines.push(''); - } - - if (customOperations.length > 0) { - lines.push('## CUSTOM OPERATIONS'); - lines.push(''); - - for (const op of customOperations) { - const accessor = op.kind === 'query' ? 'query' : 'mutation'; - - lines.push(`### OPERATION: ${op.name}`); - lines.push(''); - lines.push(op.description || op.name); - lines.push(''); - lines.push('```'); - lines.push(`TYPE: ${op.kind}`); - lines.push(`ACCESS: db.${accessor}.${op.name}`); - if (op.args.length > 0) { - lines.push( - `USAGE: db.${accessor}.${op.name}({ ${op.args.map((a) => `${a.name}: `).join(', ')} }).execute()`, - ); - lines.push(''); - lines.push('INPUT:'); - for (const arg of op.args) { - lines.push(` ${arg.name}: ${formatArgType(arg)}`); - } - } else { - lines.push( - `USAGE: db.${accessor}.${op.name}().execute()`, - ); - lines.push(''); - lines.push('INPUT: none'); - } - lines.push(''); - lines.push('OUTPUT: Promise'); - lines.push('```'); - lines.push(''); - } - } - - lines.push('## PATTERNS'); + lines.push('## Conventions'); lines.push(''); - lines.push('```typescript'); - lines.push('// All methods require .execute() to run'); - lines.push( - 'const result = await db.modelName.findMany({ select: { id: true } }).execute();', - ); + lines.push('- Access models via `db.` (e.g. `db.User`)'); + lines.push('- CRUD methods: `findMany`, `findOne`, `create`, `update`, `delete`'); + lines.push('- Always call `.execute()` to run the query'); + lines.push('- Custom operations via `db.query.` or `db.mutation.`'); lines.push(''); - lines.push('// Select specific fields'); - lines.push( - 'const partial = await db.modelName.findMany({ select: { id: true, name: true } }).execute();', - ); + + lines.push('## Boundaries'); lines.push(''); - lines.push('// Filter with where clause'); - lines.push( - "const filtered = await db.modelName.findMany({ select: { id: true }, where: { name: 'test' } }).execute();", - ); - lines.push('```'); + lines.push('All files in this directory are generated. Do not edit manually.'); lines.push(''); return { From 8aa71d247001586e4cb240f0aedcc7e3eb071662 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 15 Mar 2026 22:20:03 +0000 Subject: [PATCH 6/6] test: remove AGENTS.md snapshot tests (no longer needed for thin router pattern) --- .../__snapshots__/cli-generator.test.ts.snap | 872 ------------------ .../__tests__/codegen/cli-generator.test.ts | 31 - 2 files changed, 903 deletions(-) diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap index 343ff2312..52c49afd7 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap @@ -1,238 +1,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`cli docs generator generates CLI AGENTS.md 1`] = ` -"# myapp CLI - Agent Reference - - -> This document is structured for LLM/agent consumption. - -## OVERVIEW - -\`myapp\` is a CLI tool for interacting with a GraphQL API. -All commands output JSON to stdout. All commands accept \`--help\` or \`-h\` for usage. -Configuration is stored at \`~/.myapp/config/\` via appstash. - -## PREREQUISITES - -Before running any data commands, you must: - -1. Create a context: \`myapp context create --endpoint \` -2. Activate it: \`myapp context use \` -3. Authenticate: \`myapp auth set-token \` - -## TOOLS - -### TOOL: context - -Manage named API endpoint contexts (like kubectl contexts). - -\`\`\` -SUBCOMMANDS: - myapp context create --endpoint Create a new context - myapp context list List all contexts - myapp context use Set active context - myapp context current Show active context - myapp context delete Delete a context - -INPUT: - name: string (required) - Context identifier - endpoint: string (required for create) - GraphQL endpoint URL - -OUTPUT: JSON - create: { name, endpoint } - list: [{ name, endpoint, isCurrent, hasCredentials }] - use: { name, endpoint } - current: { name, endpoint } - delete: { deleted: name } -\`\`\` - -### TOOL: auth - -Manage authentication tokens per context. - -\`\`\` -SUBCOMMANDS: - myapp auth set-token Store bearer token for current context - myapp auth status Show auth status for all contexts - myapp auth logout Remove credentials for current context - -INPUT: - token: string (required for set-token) - Bearer token value - -OUTPUT: JSON - set-token: { context, status: "authenticated" } - status: [{ context, authenticated: boolean }] - logout: { context, status: "logged out" } -\`\`\` - -### TOOL: config - -Manage per-context key-value configuration variables. - -\`\`\` -SUBCOMMANDS: - myapp config get Get a config value - myapp config set Set a config value - myapp config list List all config values - myapp config delete Delete a config value - -INPUT: - key: string (required for get/set/delete) - Variable name - value: string (required for set) - Variable value - -OUTPUT: JSON - get: { key, value } - set: { key, value } - list: { vars: { key: value, ... } } - delete: { deleted: key } -\`\`\` - -### TOOL: car - -CRUD operations for Car records. - -\`\`\` -SUBCOMMANDS: - myapp car list List all records - myapp car get --id Get one record - myapp car create --make --model --year --isElectric - myapp car update --id [--make ] [--model ] [--year ] [--isElectric ] - myapp car delete --id Delete one record - -INPUT FIELDS: - id: UUID (primary key) - make: String - model: String - year: Int - isElectric: Boolean - createdAt: Datetime - -EDITABLE FIELDS (for create/update): - make: String - model: String - year: Int - isElectric: Boolean - -OUTPUT: JSON - list: [{ id, make, model, year, isElectric, createdAt }] - get: { id, make, model, year, isElectric, createdAt } - create: { id, make, model, year, isElectric, createdAt } - update: { id, make, model, year, isElectric, createdAt } - delete: { id } -\`\`\` - -### TOOL: driver - -CRUD operations for Driver records. - -\`\`\` -SUBCOMMANDS: - myapp driver list List all records - myapp driver get --id Get one record - myapp driver create --name --licenseNumber - myapp driver update --id [--name ] [--licenseNumber ] - myapp driver delete --id Delete one record - -INPUT FIELDS: - id: UUID (primary key) - name: String - licenseNumber: String - -EDITABLE FIELDS (for create/update): - name: String - licenseNumber: String - -OUTPUT: JSON - list: [{ id, name, licenseNumber }] - get: { id, name, licenseNumber } - create: { id, name, licenseNumber } - update: { id, name, licenseNumber } - delete: { id } -\`\`\` - -### TOOL: current-user - -Get the currently authenticated user - -\`\`\` -TYPE: query -USAGE: myapp current-user - -INPUT: none - -OUTPUT: JSON -\`\`\` - -### TOOL: login - -Authenticate a user - -\`\`\` -TYPE: mutation -USAGE: myapp login --email --password - -INPUT: - email: String (required) - password: String (required) - -OUTPUT: JSON -\`\`\` - -## WORKFLOWS - -### Initial setup - -\`\`\`bash -myapp context create dev --endpoint http://localhost:5000/graphql -myapp context use dev -myapp auth set-token eyJhbGciOiJIUzI1NiIs... -\`\`\` - -### CRUD workflow (car) - -\`\`\`bash -# List all -myapp car list - -# Create -myapp car create --make "value" --model "value" --year "value" --isElectric "value" - -# Get by id -myapp car get --id - -# Update -myapp car update --id --make "new-value" - -# Delete -myapp car delete --id -\`\`\` - -### Piping output - -\`\`\`bash -# Pretty print -myapp car list | jq '.' - -# Extract field -myapp car list | jq '.[].id' - -# Count results -myapp car list | jq 'length' -\`\`\` - -## ERROR HANDLING - -All errors are written to stderr. Exit codes: -- \`0\`: Success -- \`1\`: Error (auth failure, not found, validation error, network error) - -Common errors: -- "No active context": Run \`context use \` first -- "Not authenticated": Run \`auth set-token \` first -- "Record not found": The requested ID does not exist -" -`; - exports[`cli docs generator generates CLI README 1`] = ` "# myapp CLI @@ -1582,209 +1349,6 @@ export function getClient(contextName?: string) { }" `; -exports[`hooks docs generator generates hooks AGENTS.md 1`] = ` -"# React Query Hooks - Agent Reference - - -> This document is structured for LLM/agent consumption. - -## OVERVIEW - -React Query hooks wrapping ORM operations for data fetching and mutations. -All query hooks return \`UseQueryResult\`. All mutation hooks return \`UseMutationResult\`. - -## SETUP - -\`\`\`typescript -import { configure } from './hooks'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; - -configure({ endpoint: 'https://api.example.com/graphql' }); -const queryClient = new QueryClient(); -// Wrap app in -\`\`\` - -## HOOKS - -### HOOK: useCarsQuery - -List all cars. - -\`\`\` -TYPE: query -USAGE: useCarsQuery({ selection: { fields: { ... } } }) - -INPUT: - selection: { fields: Record } - Fields to select - -OUTPUT: UseQueryResult> -\`\`\` - -### HOOK: useCarQuery - -Get a single car by id. - -\`\`\` -TYPE: query -USAGE: useCarQuery({ id: '', selection: { fields: { ... } } }) - -INPUT: - id: string (required) - selection: { fields: Record } - Fields to select - -OUTPUT: UseQueryResult<{ - id: string - make: string - model: string - year: number - isElectric: boolean - createdAt: string -}> -\`\`\` - -### HOOK: useCreateCarMutation - -Create a new car. - -\`\`\` -TYPE: mutation -USAGE: const { mutate } = useCreateCarMutation({ selection: { fields: { ... } } }) - -OUTPUT: UseMutationResult -\`\`\` - -### HOOK: useUpdateCarMutation - -Update an existing car. - -\`\`\` -TYPE: mutation -USAGE: const { mutate } = useUpdateCarMutation({ selection: { fields: { ... } } }) - -OUTPUT: UseMutationResult -\`\`\` - -### HOOK: useDeleteCarMutation - -Delete a car. - -\`\`\` -TYPE: mutation -USAGE: const { mutate } = useDeleteCarMutation({}) - -OUTPUT: UseMutationResult -\`\`\` - -### HOOK: useDriversQuery - -List all drivers. - -\`\`\` -TYPE: query -USAGE: useDriversQuery({ selection: { fields: { ... } } }) - -INPUT: - selection: { fields: Record } - Fields to select - -OUTPUT: UseQueryResult> -\`\`\` - -### HOOK: useDriverQuery - -Get a single driver by id. - -\`\`\` -TYPE: query -USAGE: useDriverQuery({ id: '', selection: { fields: { ... } } }) - -INPUT: - id: string (required) - selection: { fields: Record } - Fields to select - -OUTPUT: UseQueryResult<{ - id: string - name: string - licenseNumber: string -}> -\`\`\` - -### HOOK: useCreateDriverMutation - -Create a new driver. - -\`\`\` -TYPE: mutation -USAGE: const { mutate } = useCreateDriverMutation({ selection: { fields: { ... } } }) - -OUTPUT: UseMutationResult -\`\`\` - -### HOOK: useUpdateDriverMutation - -Update an existing driver. - -\`\`\` -TYPE: mutation -USAGE: const { mutate } = useUpdateDriverMutation({ selection: { fields: { ... } } }) - -OUTPUT: UseMutationResult -\`\`\` - -### HOOK: useDeleteDriverMutation - -Delete a driver. - -\`\`\` -TYPE: mutation -USAGE: const { mutate } = useDeleteDriverMutation({}) - -OUTPUT: UseMutationResult -\`\`\` - -## CUSTOM OPERATION HOOKS - -### HOOK: useCurrentUserQuery - -Get the currently authenticated user - -\`\`\` -TYPE: query -USAGE: useCurrentUserQuery() - -INPUT: none - -OUTPUT: UseQueryResult -\`\`\` - -### HOOK: useLoginMutation - -Authenticate a user - -\`\`\` -TYPE: mutation -USAGE: const { mutate } = useLoginMutation() - mutate({ email: , password: }) - -INPUT: - email: String (required) - password: String (required) - -OUTPUT: UseMutationResult -\`\`\` -" -`; - exports[`hooks docs generator generates hooks README 1`] = ` "# React Query Hooks @@ -2080,308 +1644,6 @@ See the \`references/\` directory for detailed per-entity API documentation: " `; -exports[`multi-target cli docs generates multi-target AGENTS.md 1`] = ` -"# myapp CLI - Agent Reference - - -> This document is structured for LLM/agent consumption. - -## OVERVIEW - -\`myapp\` is a unified multi-target CLI for interacting with multiple GraphQL APIs. -All commands output JSON to stdout. All commands accept \`--help\` or \`-h\` for usage. -Configuration is stored at \`~/.myapp/config/\` via appstash. - -TARGETS: - auth: http://auth.localhost/graphql - members: http://members.localhost/graphql - app: http://app.localhost/graphql - -COMMAND FORMAT: - myapp : [flags] Target-specific commands - myapp context [flags] Context management - myapp credentials [flags] Authentication - myapp config [flags] Config key-value store - -## PREREQUISITES - -Before running any data commands, you must: - -1. Create a context: \`myapp context create \` - (prompts for per-target endpoints, defaults baked from config) -2. Activate it: \`myapp context use \` -3. Authenticate: \`myapp credentials set-token \` - -For local development, create a context accepting all defaults: - -\`\`\`bash -myapp context create local -myapp context use local -myapp credentials set-token -\`\`\` - -## TOOLS - -### TOOL: context - -Manage named API endpoint contexts. Each context stores per-target endpoint overrides. - -\`\`\` -SUBCOMMANDS: - myapp context create Create a new context - myapp context list List all contexts - myapp context use Set active context - myapp context current Show active context - myapp context delete Delete a context - -CREATE OPTIONS: - --auth-endpoint: string (default: http://auth.localhost/graphql) - --members-endpoint: string (default: http://members.localhost/graphql) - --app-endpoint: string (default: http://app.localhost/graphql) - -OUTPUT: JSON - create: { name, endpoint, targets } - list: [{ name, endpoint, isCurrent, hasCredentials }] - use: { name, endpoint } - current: { name, endpoint } - delete: { deleted: name } -\`\`\` - -### TOOL: credentials - -Manage authentication tokens per context. One shared token across all targets. - -\`\`\` -SUBCOMMANDS: - myapp credentials set-token Store bearer token for current context - myapp credentials status Show auth status for all contexts - myapp credentials logout Remove credentials for current context - -INPUT: - token: string (required for set-token) - Bearer token value - -OUTPUT: JSON - set-token: { context, status: "authenticated" } - status: [{ context, authenticated: boolean }] - logout: { context, status: "logged out" } -\`\`\` - -### TOOL: config - -Manage per-context key-value configuration variables. - -\`\`\` -SUBCOMMANDS: - myapp config get Get a config value - myapp config set Set a config value - myapp config list List all config values - myapp config delete Delete a config value - -INPUT: - key: string (required for get/set/delete) - Variable name - value: string (required for set) - Variable value - -OUTPUT: JSON - get: { key, value } - set: { key, value } - list: { vars: { key: value, ... } } - delete: { deleted: key } -\`\`\` - -### TOOL: helpers (SDK) - -Typed client factories for use in scripts and services (generated helpers.ts). -Resolves credentials via: appstash store -> env vars -> throw. - -\`\`\` -FACTORIES: - createAuthClient(contextName?) Create a configured auth ORM client - createMembersClient(contextName?) Create a configured members ORM client - createAppClient(contextName?) Create a configured app ORM client - -USAGE: - import { createAuthClient } from './helpers'; - const client = createAuthClient(); - -CREDENTIAL RESOLUTION: - 1. appstash store (~/.myapp/config/) - 2. env vars (MYAPP_TOKEN, MYAPP__ENDPOINT) - 3. throws with actionable error message -\`\`\` - -### TOOL: auth:user - -CRUD operations for User records (auth target). - -\`\`\` -SUBCOMMANDS: - myapp auth:user list List all records - myapp auth:user get --id Get one record - myapp auth:user create --email --name - myapp auth:user update --id [--email ] [--name ] - myapp auth:user delete --id Delete one record - -INPUT FIELDS: - id: UUID (primary key) - email: String - name: String - -EDITABLE FIELDS (for create/update): - email: String - name: String - -OUTPUT: JSON - list: [{ id, email, name }] - get: { id, email, name } - create: { id, email, name } - update: { id, email, name } - delete: { id } -\`\`\` - -### TOOL: auth:current-user - -Get the currently authenticated user - -\`\`\` -TYPE: query -USAGE: myapp auth:current-user - -INPUT: none - -OUTPUT: JSON -\`\`\` - -### TOOL: auth:login - -Authenticate a user - -\`\`\` -TYPE: mutation -USAGE: myapp auth:login --email --password - -INPUT: - email: String (required) - password: String (required) - -FLAGS: - --save-token: boolean - Auto-save returned token to credentials - -OUTPUT: JSON -\`\`\` - -### TOOL: members:member - -CRUD operations for Member records (members target). - -\`\`\` -SUBCOMMANDS: - myapp members:member list List all records - myapp members:member get --id Get one record - myapp members:member create --role - myapp members:member update --id [--role ] - myapp members:member delete --id Delete one record - -INPUT FIELDS: - id: UUID (primary key) - role: String - -EDITABLE FIELDS (for create/update): - role: String - -OUTPUT: JSON - list: [{ id, role }] - get: { id, role } - create: { id, role } - update: { id, role } - delete: { id } -\`\`\` - -### TOOL: app:car - -CRUD operations for Car records (app target). - -\`\`\` -SUBCOMMANDS: - myapp app:car list List all records - myapp app:car get --id Get one record - myapp app:car create --make --model --year --isElectric - myapp app:car update --id [--make ] [--model ] [--year ] [--isElectric ] - myapp app:car delete --id Delete one record - -INPUT FIELDS: - id: UUID (primary key) - make: String - model: String - year: Int - isElectric: Boolean - createdAt: Datetime - -EDITABLE FIELDS (for create/update): - make: String - model: String - year: Int - isElectric: Boolean - -OUTPUT: JSON - list: [{ id, make, model, year, isElectric, createdAt }] - get: { id, make, model, year, isElectric, createdAt } - create: { id, make, model, year, isElectric, createdAt } - update: { id, make, model, year, isElectric, createdAt } - delete: { id } -\`\`\` - -## WORKFLOWS - -### Initial setup - -\`\`\`bash -myapp context create dev -myapp context use dev -myapp credentials set-token eyJhbGciOiJIUzI1NiIs... -\`\`\` - -### Switch environment - -\`\`\`bash -myapp context create production \\ - --auth-endpoint https://auth.prod.example.com/graphql \\ - --members-endpoint https://members.prod.example.com/graphql \\ - --app-endpoint https://app.prod.example.com/graphql -myapp context use production -\`\`\` - -### CRUD workflow (auth:user) - -\`\`\`bash -myapp auth:user list -myapp auth:user create --email "value" --name "value" -myapp auth:user get --id -myapp auth:user update --id --email "new-value" -myapp auth:user delete --id -\`\`\` - -### Piping output - -\`\`\`bash -myapp auth:user list | jq '.' -myapp auth:user list | jq '.[].id' -myapp auth:user list | jq 'length' -\`\`\` - -## ERROR HANDLING - -All errors are written to stderr. Exit codes: -- \`0\`: Success -- \`1\`: Error (auth failure, not found, validation error, network error) - -Common errors: -- "No active context": Run \`context use \` first -- "Not authenticated": Run \`credentials set-token \` first -- "Unknown target": The target name is not recognized -- "Record not found": The requested ID does not exist -" -`; - exports[`multi-target cli docs generates multi-target MCP tools 1`] = ` [ { @@ -4941,140 +4203,6 @@ async function handleDelete(argv: Partial>, prompter: In }" `; -exports[`orm docs generator generates ORM AGENTS.md 1`] = ` -"# ORM Client - Agent Reference - - -> This document is structured for LLM/agent consumption. - -## OVERVIEW - -Prisma-like ORM client for interacting with a GraphQL API. -All methods return a query builder. Call \`.execute()\` to run the query. - -## SETUP - -\`\`\`typescript -import { createClient } from './orm'; - -const db = createClient({ - endpoint: 'https://api.example.com/graphql', - headers: { Authorization: 'Bearer ' }, -}); -\`\`\` - -## MODELS - -### MODEL: car - -Access: \`db.car\` - -\`\`\` -METHODS: - db.car.findMany({ select, where?, orderBy?, first?, offset? }) - db.car.findOne({ id, select }) - db.car.create({ data: { make, model, year, isElectric }, select }) - db.car.update({ where: { id }, data, select }) - db.car.delete({ where: { id } }) - -FIELDS: - id: string (primary key) - make: string - model: string - year: number - isElectric: boolean - createdAt: string - -EDITABLE FIELDS: - make: string - model: string - year: number - isElectric: boolean - -OUTPUT: Promise - findMany: [{ id, make, model, year, isElectric, createdAt }] - findOne: { id, make, model, year, isElectric, createdAt } - create: { id, make, model, year, isElectric, createdAt } - update: { id, make, model, year, isElectric, createdAt } - delete: { id } -\`\`\` - -### MODEL: driver - -Access: \`db.driver\` - -\`\`\` -METHODS: - db.driver.findMany({ select, where?, orderBy?, first?, offset? }) - db.driver.findOne({ id, select }) - db.driver.create({ data: { name, licenseNumber }, select }) - db.driver.update({ where: { id }, data, select }) - db.driver.delete({ where: { id } }) - -FIELDS: - id: string (primary key) - name: string - licenseNumber: string - -EDITABLE FIELDS: - name: string - licenseNumber: string - -OUTPUT: Promise - findMany: [{ id, name, licenseNumber }] - findOne: { id, name, licenseNumber } - create: { id, name, licenseNumber } - update: { id, name, licenseNumber } - delete: { id } -\`\`\` - -## CUSTOM OPERATIONS - -### OPERATION: currentUser - -Get the currently authenticated user - -\`\`\` -TYPE: query -ACCESS: db.query.currentUser -USAGE: db.query.currentUser().execute() - -INPUT: none - -OUTPUT: Promise -\`\`\` - -### OPERATION: login - -Authenticate a user - -\`\`\` -TYPE: mutation -ACCESS: db.mutation.login -USAGE: db.mutation.login({ email: , password: }).execute() - -INPUT: - email: String (required) - password: String (required) - -OUTPUT: Promise -\`\`\` - -## PATTERNS - -\`\`\`typescript -// All methods require .execute() to run -const result = await db.modelName.findMany({ select: { id: true } }).execute(); - -// Select specific fields -const partial = await db.modelName.findMany({ select: { id: true, name: true } }).execute(); - -// Filter with where clause -const filtered = await db.modelName.findMany({ select: { id: true }, where: { name: 'test' } }).execute(); -\`\`\` -" -`; - exports[`orm docs generator generates ORM README 1`] = ` "# ORM Client diff --git a/graphql/codegen/src/__tests__/codegen/cli-generator.test.ts b/graphql/codegen/src/__tests__/codegen/cli-generator.test.ts index a8584ec26..f4372e7a6 100644 --- a/graphql/codegen/src/__tests__/codegen/cli-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/cli-generator.test.ts @@ -1,11 +1,9 @@ import { generateCli, generateMultiTargetCli, resolveBuiltinNames } from '../../core/codegen/cli'; import { generateReadme as generateCliReadme, - generateAgentsDocs as generateCliAgentsDocs, getCliMcpTools, generateSkills as generateCliSkills, generateMultiTargetReadme, - generateMultiTargetAgentsDocs, getMultiTargetCliMcpTools, generateMultiTargetSkills, } from '../../core/codegen/cli/docs-generator'; @@ -13,13 +11,11 @@ import type { MultiTargetDocsInput } from '../../core/codegen/cli/docs-generator import { resolveDocsConfig } from '../../core/codegen/docs-utils'; import { generateOrmReadme, - generateOrmAgentsDocs, getOrmMcpTools, generateOrmSkills, } from '../../core/codegen/orm/docs-generator'; import { generateHooksReadme, - generateHooksAgentsDocs, getHooksMcpTools, generateHooksSkills, } from '../../core/codegen/hooks-docs-generator'; @@ -268,12 +264,6 @@ describe('cli docs generator', () => { expect(readme.content).toMatchSnapshot(); }); - it('generates CLI AGENTS.md', () => { - const agents = generateCliAgentsDocs([carTable, driverTable], allCustomOps, 'myapp'); - expect(agents.fileName).toBe('AGENTS.md'); - expect(agents.content).toMatchSnapshot(); - }); - it('generates CLI MCP tools', () => { const tools = getCliMcpTools([carTable, driverTable], allCustomOps, 'myapp'); expect(tools.length).toBeGreaterThan(0); @@ -300,12 +290,6 @@ describe('orm docs generator', () => { expect(readme.content).toMatchSnapshot(); }); - it('generates ORM AGENTS.md', () => { - const agents = generateOrmAgentsDocs([carTable, driverTable], allCustomOps); - expect(agents.fileName).toBe('AGENTS.md'); - expect(agents.content).toMatchSnapshot(); - }); - it('generates ORM MCP tools', () => { const tools = getOrmMcpTools([carTable, driverTable], allCustomOps); expect(tools.length).toBeGreaterThan(0); @@ -332,12 +316,6 @@ describe('hooks docs generator', () => { expect(readme.content).toMatchSnapshot(); }); - it('generates hooks AGENTS.md', () => { - const agents = generateHooksAgentsDocs([carTable, driverTable], allCustomOps); - expect(agents.fileName).toBe('AGENTS.md'); - expect(agents.content).toMatchSnapshot(); - }); - it('generates hooks MCP tools', () => { const tools = getHooksMcpTools([carTable, driverTable], allCustomOps); expect(tools.length).toBeGreaterThan(0); @@ -735,15 +713,6 @@ describe('multi-target cli docs', () => { expect(readme.content).toMatchSnapshot(); }); - it('generates multi-target AGENTS.md', () => { - const agents = generateMultiTargetAgentsDocs(docsInput); - expect(agents.fileName).toBe('AGENTS.md'); - expect(agents.content).toContain('auth:user'); - expect(agents.content).toContain('members:member'); - expect(agents.content).toContain('app:car'); - expect(agents.content).toMatchSnapshot(); - }); - it('generates multi-target MCP tools', () => { const tools = getMultiTargetCliMcpTools(docsInput); expect(tools.length).toBeGreaterThan(0);