kimi-code/packages/agent-core-v2/scripts/lib/jsonSchema.mts
Haozhe 5240b5c83c
feat(agent-core-v2): add generated config and wire-protocol manifests (#2086)
* feat(agent-core-v2): add generated config section manifest

- add scripts/gen-config-manifest.mts: drains the live
  registerConfigSection / registerConfigOverlay contributions and renders
  docs/config-manifest.toml in the on-disk config.toml shape (owner,
  scope, registered defaults, env bindings, schema fields)
- add a gen:config-manifest package script (--check mode included) and a
  freshness test that rebuilds the manifest and compares byte-for-byte
- point the agent-core-dev config skill and the package AGENTS.md at the
  generated manifest instead of the stale hand-maintained ownership map

* feat(agent-core-v2): add generated wire-protocol manifest

- add scripts/gen-wire-manifest.mts to generate docs/wire-manifest.d.ts
  from defineOp registrations (payload interfaces, persist policy,
  toEvent, cross-reducers) plus a WirePayloadMap
- extract shared JSON Schema helpers from gen-config-manifest.mts into
  scripts/lib/jsonSchema.mts
- add gen:wire-manifest script and wireManifest.test.ts freshness check
- document the manifest in packages/agent-core-v2/AGENTS.md

* fix(agent-core-v2): keep array-of-tables manifest sections fully commented

A bare `[hooks]` header parses as a plain table, which array sections
reject on load; emit only the commented `[[hooks]]` shape so the
manifest matches the on-disk config.toml shape it documents.

Addresses a Codex review comment on PR #2086.
2026-07-23 11:17:10 +08:00

99 lines
3.3 KiB
TypeScript

/**
* Shared JSON-schema helpers for the manifest generators
* (`gen-config-manifest.mts`, `gen-wire-manifest.mts`).
*
* Both generators drain runtime registries that carry zod schemas and render
* field/type sketches from their JSON Schema projection.
*/
import { z } from 'zod';
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function truncate(text: string, max = 100): string {
return text.length > max ? `${text.slice(0, max - 1)}` : text;
}
/** Property access shape of a JSON Schema node (avoids index-signature access). */
export interface JsonSchema {
readonly $ref?: unknown;
readonly $defs?: unknown;
readonly const?: unknown;
readonly enum?: unknown;
readonly anyOf?: unknown;
readonly oneOf?: unknown;
readonly type?: unknown;
readonly items?: unknown;
readonly properties?: unknown;
readonly required?: unknown;
readonly additionalProperties?: unknown;
readonly default?: unknown;
}
export function asJsonSchema(value: unknown): JsonSchema | undefined {
return isRecord(value) ? (value as JsonSchema) : undefined;
}
/** Resolve a `#/$defs/<name>` reference against the root schema. */
export function resolveRef(schema: unknown, root: JsonSchema): unknown {
const s = asJsonSchema(schema);
if (typeof s?.$ref === 'string' && s.$ref.startsWith('#/$defs/')) {
const defs = asJsonSchema(root.$defs);
const name = s.$ref.slice('#/$defs/'.length);
if (defs !== undefined && isRecord(defs) && name in defs) {
return (defs as Record<string, unknown>)[name];
}
}
return schema;
}
/** One-line type description of a JSON Schema node (`"a" | "b"`, `Foo[]`, …). */
export function describeType(
schema: unknown,
quoteString: (raw: string) => string = (s) => JSON.stringify(s),
): string {
const s = asJsonSchema(schema);
if (s === undefined) return 'any';
if (s.$ref !== undefined) {
return typeof s.$ref === 'string' ? (s.$ref.split('/').pop() ?? 'any') : 'any';
}
if (s.const !== undefined) {
return truncate(
typeof s.const === 'string' ? quoteString(s.const) : JSON.stringify(s.const),
40,
);
}
if (Array.isArray(s.enum)) {
return s.enum
.map((v) => (typeof v === 'string' ? quoteString(v) : JSON.stringify(v)))
.join(' | ');
}
for (const combiner of ['anyOf', 'oneOf'] as const) {
const subs = s[combiner];
if (Array.isArray(subs)) return subs.map((sub) => describeType(sub, quoteString)).join(' | ');
}
if (s.type === 'array') return `${describeType(s.items, quoteString)}[]`;
if (s.type === 'object') {
// Named sub-tables (zod objects emit `additionalProperties: false`) are
// rendered by the caller; only a schema-valued additionalProperties marks
// a true record.
if (isRecord(s.properties)) return 'object';
if (isRecord(s.additionalProperties)) {
return `record<string, ${describeType(s.additionalProperties, quoteString)}>`;
}
return 'object';
}
if (typeof s.type === 'string') return s.type;
return 'any';
}
/** Project a zod schema to JSON Schema; `undefined` when it uses transforms. */
export function toJsonSchema(schema: unknown): JsonSchema | undefined {
try {
return z.toJSONSchema(schema as never) as JsonSchema;
} catch {
return undefined;
}
}