@quonfig/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ export declare class ZodToStringMapper extends ZodBaseMapper {
9
9
  function(input: string, output: string): string;
10
10
  functionArguments(value?: $ZodFunctionArgs): string;
11
11
  functionReturns(value: $ZodFunctionOut): string;
12
+ intersection(left: string, right: string): string;
12
13
  literal(value: string | number | boolean | null): string;
13
14
  never(): string;
14
15
  null(): string;
@@ -18,7 +19,7 @@ export declare class ZodToStringMapper extends ZodBaseMapper {
18
19
  record(keyType: string, valueType: string): string;
19
20
  renderField(type: z.ZodTypeAny, key?: string): string;
20
21
  string(): string;
21
- tuple(wrappedTypes: string[]): string;
22
+ tuple(wrappedTypes: string[], rest?: string): string;
22
23
  undefined(): string;
23
24
  union(wrappedTypes: string[]): string;
24
25
  unknown(): string;
@@ -31,6 +31,9 @@ export class ZodToStringMapper extends ZodBaseMapper {
31
31
  const mapper = new ZodToStringMapper();
32
32
  return mapper.resolveType(value);
33
33
  }
34
+ intersection(left, right) {
35
+ return `z.intersection(${left}, ${right})`;
36
+ }
34
37
  literal(value) {
35
38
  return `z.literal(${JSON.stringify(value)})`;
36
39
  }
@@ -72,8 +75,9 @@ export class ZodToStringMapper extends ZodBaseMapper {
72
75
  string() {
73
76
  return 'z.string()';
74
77
  }
75
- tuple(wrappedTypes) {
76
- return `z.tuple([${wrappedTypes.join(', ')}])`;
78
+ tuple(wrappedTypes, rest) {
79
+ const base = `z.tuple([${wrappedTypes.join(', ')}])`;
80
+ return rest === undefined ? base : `${base}.rest(${rest})`;
77
81
  }
78
82
  undefined() {
79
83
  return 'z.undefined()';
@@ -1,9 +1,11 @@
1
1
  import { z } from 'zod';
2
+ import { $ZodType } from 'zod/v4/core';
2
3
  import { ZodTypeSupported } from '../types.js';
3
4
  import { ZodBaseMapper } from './zod-base-mapper.js';
4
5
  export type ZodToTypescriptMapperTarget = 'accessor' | 'raw';
5
6
  export declare class ZodToTypescriptMapper extends ZodBaseMapper {
6
7
  private fieldName;
8
+ private metaCaptureOpen;
7
9
  private metaDescription;
8
10
  private optionalProperty;
9
11
  private target;
@@ -18,6 +20,7 @@ export declare class ZodToTypescriptMapper extends ZodBaseMapper {
18
20
  function(args: string, returns: string): string;
19
21
  functionArguments(value?: z.ZodTuple): string;
20
22
  functionReturns(value: z.ZodTypeAny): string;
23
+ intersection(left: string, right: string): string;
21
24
  literal(value: string | number | boolean | null): string;
22
25
  never(): string;
23
26
  null(): string;
@@ -26,10 +29,11 @@ export declare class ZodToTypescriptMapper extends ZodBaseMapper {
26
29
  optional(wrappedType: string): string;
27
30
  record(keyType: string, valueType: string): string;
28
31
  renderField(type: ZodTypeSupported): string;
32
+ resolveType(type: $ZodType): string;
29
33
  string(): string;
30
- tuple(wrappedTypes: string[]): string;
34
+ tuple(wrappedTypes: string[], rest?: string): string;
31
35
  undefined(): string;
32
36
  union(wrappedTypes: string[]): string;
33
37
  unknown(): string;
34
- protected withMeta(description: string, resolveType: () => string): string;
38
+ protected withMeta(_description: string, resolveType: () => string): string;
35
39
  }
@@ -1,6 +1,10 @@
1
+ import * as introspect from '../zod-introspection.js';
1
2
  import { ZodBaseMapper } from './zod-base-mapper.js';
2
3
  export class ZodToTypescriptMapper extends ZodBaseMapper {
3
4
  fieldName;
5
+ // A field's comment comes from its own meta, looking through optional/nullable/default wrappers only.
6
+ // Meta deeper inside (a union branch title, an array item description) must not become the field's comment.
7
+ metaCaptureOpen = true;
4
8
  metaDescription = undefined;
5
9
  optionalProperty;
6
10
  target;
@@ -41,6 +45,9 @@ export class ZodToTypescriptMapper extends ZodBaseMapper {
41
45
  const mapper = new ZodToTypescriptMapper();
42
46
  return mapper.resolveType(value);
43
47
  }
48
+ intersection(left, right) {
49
+ return [left, right].map((t) => (t.includes(' | ') || t.includes('=>') ? `(${t})` : t)).join(' & ');
50
+ }
44
51
  literal(value) {
45
52
  return JSON.stringify(value);
46
53
  }
@@ -102,11 +109,34 @@ export class ZodToTypescriptMapper extends ZodBaseMapper {
102
109
  result += `"${this.fieldName}"${this.optionalProperty ? '?' : ''}: ${resolved}`;
103
110
  return result;
104
111
  }
112
+ resolveType(type) {
113
+ const wasOpen = this.metaCaptureOpen;
114
+ if (wasOpen) {
115
+ const description = introspect.getMetaDescription(type);
116
+ if (description) {
117
+ this.metaDescription = description;
118
+ }
119
+ }
120
+ this.metaCaptureOpen =
121
+ wasOpen && (introspect.isOptional(type) || introspect.isNullable(type) || introspect.isDefault(type));
122
+ try {
123
+ return super.resolveType(type);
124
+ }
125
+ finally {
126
+ this.metaCaptureOpen = wasOpen;
127
+ }
128
+ }
105
129
  string() {
106
130
  return 'string';
107
131
  }
108
- tuple(wrappedTypes) {
109
- return `[${wrappedTypes.join(', ')}]`;
132
+ tuple(wrappedTypes, rest) {
133
+ const items = [...wrappedTypes];
134
+ if (rest !== undefined) {
135
+ // `...A | B[]` would parse as `...(A | (B[]))`, so wrap anything that isn't a simple type
136
+ const restType = /^[\w ,<>]+$/.test(rest) ? rest : `(${rest})`;
137
+ items.push(`...${restType}[]`);
138
+ }
139
+ return `[${items.join(', ')}]`;
110
140
  }
111
141
  undefined() {
112
142
  return 'undefined';
@@ -125,9 +155,8 @@ export class ZodToTypescriptMapper extends ZodBaseMapper {
125
155
  unknown() {
126
156
  return 'unknown';
127
157
  }
128
- withMeta(description, resolveType) {
129
- // Store the description to be used when rendering the field
130
- this.metaDescription = description;
158
+ withMeta(_description, resolveType) {
159
+ // The description is captured in resolveType, which knows whether it belongs to this field
131
160
  return resolveType();
132
161
  }
133
162
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { $ZodType } from 'zod/v4/core';
2
3
  import { ZodTypeSupported } from '../types.js';
3
4
  import { ZodBaseMapper } from './zod-base-mapper.js';
4
5
  export declare class ZodToTypescriptReturnValueMapper extends ZodBaseMapper {
@@ -6,9 +7,9 @@ export declare class ZodToTypescriptReturnValueMapper extends ZodBaseMapper {
6
7
  private FUNCTION_ARGUMENTS_NAME;
7
8
  private metaDescription;
8
9
  private returnTypePropertyPath;
9
- constructor({ fieldName, returnTypePropertyPath }?: {
10
+ constructor({ fieldName, returnTypePropertyPath, }?: {
10
11
  fieldName?: string;
11
- returnTypePropertyPath?: string[];
12
+ returnTypePropertyPath?: (number | string)[];
12
13
  });
13
14
  any(): string;
14
15
  array(): string;
@@ -17,6 +18,7 @@ export declare class ZodToTypescriptReturnValueMapper extends ZodBaseMapper {
17
18
  function(args: string, returns: string): string;
18
19
  functionArguments(value?: z.ZodTuple): string;
19
20
  functionReturns(): string;
21
+ intersection(): string;
20
22
  literal(): string;
21
23
  never(): string;
22
24
  null(): string;
@@ -25,8 +27,9 @@ export declare class ZodToTypescriptReturnValueMapper extends ZodBaseMapper {
25
27
  optional(wrappedType: string): string;
26
28
  record(): string;
27
29
  renderField(type: ZodTypeSupported): string;
30
+ protected resolveTupleItem(item: $ZodType, index: number): string;
28
31
  string(): string;
29
- tuple(wrappedTypes: string[]): string;
32
+ tuple(wrappedTypes: string[], rest?: string): string;
30
33
  undefined(): string;
31
34
  union(wrappedTypes: string[]): string;
32
35
  unknown(): string;
@@ -3,8 +3,9 @@ export class ZodToTypescriptReturnValueMapper extends ZodBaseMapper {
3
3
  fieldName;
4
4
  FUNCTION_ARGUMENTS_NAME = 'params';
5
5
  metaDescription = undefined;
6
+ // Strings are property names, numbers are tuple indexes
6
7
  returnTypePropertyPath;
7
- constructor({ fieldName, returnTypePropertyPath } = {}) {
8
+ constructor({ fieldName, returnTypePropertyPath, } = {}) {
8
9
  super();
9
10
  this.fieldName = fieldName;
10
11
  this.returnTypePropertyPath = returnTypePropertyPath ?? [];
@@ -36,6 +37,10 @@ export class ZodToTypescriptReturnValueMapper extends ZodBaseMapper {
36
37
  functionReturns() {
37
38
  return `Mustache.render(raw${this.printPropertyPath()} ?? "", ${this.FUNCTION_ARGUMENTS_NAME})`;
38
39
  }
40
+ intersection() {
41
+ // Explicitly not supporting navigation into intersections, pass the raw value through
42
+ return `raw${this.printPropertyPath()}`;
43
+ }
39
44
  literal() {
40
45
  return `raw${this.printPropertyPath()}`;
41
46
  }
@@ -102,18 +107,24 @@ export class ZodToTypescriptReturnValueMapper extends ZodBaseMapper {
102
107
  this.metaDescription = undefined;
103
108
  return result;
104
109
  }
110
+ resolveTupleItem(item, index) {
111
+ const mapper = new ZodToTypescriptReturnValueMapper({
112
+ returnTypePropertyPath: [...this.returnTypePropertyPath, index],
113
+ });
114
+ return mapper.resolveType(item);
115
+ }
105
116
  string() {
106
117
  return `raw${this.printPropertyPath()}`;
107
118
  }
108
- tuple(wrappedTypes) {
109
- const tupleNavigation = wrappedTypes.map((wt, index) => {
110
- let massagedWrappedType = wt;
111
- if (massagedWrappedType !== 'raw') {
112
- // Remove trailing ! from the wrapped type
113
- massagedWrappedType = massagedWrappedType.replace(/!$/, '');
114
- }
115
- return `${massagedWrappedType}?.[${index}]!`;
116
- });
119
+ tuple(wrappedTypes, rest) {
120
+ // Each fixed item was already resolved at its own index by resolveTupleItem
121
+ const tupleNavigation = [...wrappedTypes];
122
+ if (rest !== undefined) {
123
+ // Pass the remaining items through untouched; truncating to the fixed items would drop data at runtime.
124
+ // Mustache is never extracted inside arrays, so the rest needs no per-item mapping.
125
+ const path = this.printPropertyPath().replace(/!$/, '');
126
+ tupleNavigation.push(`...(raw${path}?.slice(${wrappedTypes.length}) ?? [])`);
127
+ }
117
128
  return `[${tupleNavigation.join(`, `)}]`;
118
129
  }
119
130
  undefined() {
@@ -140,7 +151,7 @@ export class ZodToTypescriptReturnValueMapper extends ZodBaseMapper {
140
151
  return '';
141
152
  }
142
153
  // Always safe navigate the path to ensure we don't throw an error if the property doesn't exist
143
- const path = paths.reduce((acc, part) => `${acc}?.['${part}']`, '');
154
+ const path = paths.reduce((acc, part) => (typeof part === 'number' ? `${acc}?.[${part}]` : `${acc}?.['${part}']`), '');
144
155
  // To satisfy TypeScript's type system, tell it the value is always defined
145
156
  return `${path}!`;
146
157
  }
@@ -12,6 +12,7 @@ export declare function isAny(schema: $ZodType): schema is z.ZodAny;
12
12
  export declare function isArray(schema: $ZodType): schema is z.ZodArray<z.ZodTypeAny>;
13
13
  export declare function isObject(schema: $ZodType): schema is z.ZodObject<z.ZodRawShape>;
14
14
  export declare function isTuple(schema: $ZodType): schema is z.ZodTuple;
15
+ export declare function isIntersection(schema: $ZodType): schema is z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>;
15
16
  export declare function isRecord(schema: $ZodType): schema is z.ZodRecord;
16
17
  export declare function isEnum(schema: $ZodType): schema is z.ZodEnum<util.EnumLike>;
17
18
  export declare function isUnion(schema: $ZodType): schema is z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>;
@@ -31,6 +32,17 @@ export declare function getObjectShape(schema: z.ZodObject<z.ZodRawShape>): z.Zo
31
32
  * Gets the items from a tuple schema
32
33
  */
33
34
  export declare function getTupleItems(schema: z.ZodTuple): readonly $ZodType[];
35
+ /**
36
+ * Gets the rest element from a tuple schema (`z.tuple([...]).rest(x)`), or undefined when it has none
37
+ */
38
+ export declare function getTupleRest(schema: z.ZodTuple): $ZodType | undefined;
39
+ /**
40
+ * Gets both sides of an intersection schema
41
+ */
42
+ export declare function getIntersectionTypes(schema: z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>): {
43
+ left: $ZodType;
44
+ right: $ZodType;
45
+ };
34
46
  /**
35
47
  * Gets the key and value types from a record schema
36
48
  */
@@ -38,6 +38,9 @@ export function isObject(schema) {
38
38
  export function isTuple(schema) {
39
39
  return schema instanceof z.ZodTuple;
40
40
  }
41
+ export function isIntersection(schema) {
42
+ return schema instanceof z.ZodIntersection;
43
+ }
41
44
  export function isRecord(schema) {
42
45
  return schema instanceof z.ZodRecord;
43
46
  }
@@ -83,6 +86,18 @@ export function getObjectShape(schema) {
83
86
  export function getTupleItems(schema) {
84
87
  return schema.def.items;
85
88
  }
89
+ /**
90
+ * Gets the rest element from a tuple schema (`z.tuple([...]).rest(x)`), or undefined when it has none
91
+ */
92
+ export function getTupleRest(schema) {
93
+ return schema.def.rest ?? undefined;
94
+ }
95
+ /**
96
+ * Gets both sides of an intersection schema
97
+ */
98
+ export function getIntersectionTypes(schema) {
99
+ return { left: schema.def.left, right: schema.def.right };
100
+ }
86
101
  /**
87
102
  * Gets the key and value types from a record schema
88
103
  */
@@ -334,7 +334,7 @@ export function buildRealDeps(cmd, orgSlug) {
334
334
  },
335
335
  async validate(dir) {
336
336
  const { validateWorkspace } = await import('../verify/validate.js');
337
- const result = validateWorkspace(dir);
337
+ const result = validateWorkspace(dir, { validateValues: true });
338
338
  const errors = result.issues.filter((i) => i.severity === 'error').map((i) => i.message);
339
339
  return { errors };
340
340
  },
@@ -23,7 +23,7 @@ export default class Verify extends BaseCommand {
23
23
  async run() {
24
24
  const { args, flags } = await this.parse(Verify);
25
25
  const dir = args.path;
26
- const result = validateWorkspace(dir);
26
+ const result = validateWorkspace(dir, { validateValues: true });
27
27
  if (flags.strict) {
28
28
  // Promote warnings to errors
29
29
  for (const issue of result.issues) {
@@ -83,7 +83,7 @@ export default class WorkspaceBootstrap extends BaseCommand {
83
83
  this.log('Verifying config files...');
84
84
  try {
85
85
  const { validateWorkspace } = await import('../../verify/validate.js');
86
- const result = validateWorkspace(resolvedDir);
86
+ const result = validateWorkspace(resolvedDir, { validateValues: true });
87
87
  const errors = result.issues.filter((i) => i.severity === 'error');
88
88
  if (errors.length > 0) {
89
89
  this.log(`\nFound ${errors.length} validation error(s):\n`);
@@ -47,7 +47,7 @@ const verifyOnDisk = (dir) => {
47
47
  // qfg-52qg: client-side verify against the cumulative tree before commit
48
48
  // lands. The Gitea pre-receive hook runs against the final HEAD; failing
49
49
  // here matches what the server would reject and avoids a wasted push.
50
- const verifyResult = validateWorkspace(dir);
50
+ const verifyResult = validateWorkspace(dir, { validateValues: true });
51
51
  if (!verifyResult.valid)
52
52
  throw new MigratorVerifyError(verifyResult);
53
53
  };
@@ -509,7 +509,7 @@ const removeWorktree = async (dir, worktree) => {
509
509
  */
510
510
  const requireWorkspaceValidates = async (tree) => {
511
511
  const { validateWorkspace } = await import('../verify/validate.js');
512
- const errors = validateWorkspace(tree).issues.filter((i) => i.severity === 'error');
512
+ const errors = validateWorkspace(tree, { validateValues: true }).issues.filter((i) => i.severity === 'error');
513
513
  if (errors.length === 0)
514
514
  return;
515
515
  const shown = errors.slice(0, 10).map((i) => ` ${i.file}: ${i.message}`);
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Ajv errors -> readable `{ path, message }` violations (qfg-q5f6.1).
3
+ *
4
+ * SELF-CONTAINED ON PURPOSE. This file has no imports: it is copied verbatim
5
+ * into `cli/src/verify/` so `qfg verify`, the app server and the browser
6
+ * (list badge, view page) word every violation the same way. Change it here
7
+ * first and re-copy; do not let the two drift.
8
+ *
9
+ * It works on any error object shaped like Ajv's `ErrorObject` (keyword,
10
+ * instancePath, schemaPath, params, message), from Ajv run with
11
+ * `allErrors: true`.
12
+ *
13
+ * Tagged `oneOf` (plan 2026-09-25-advanced-schema-for-jev.md, W1): Ajv
14
+ * reports every branch's failures, which for a three-way union is mostly
15
+ * noise. When every branch is an object with the same `const` (or one-value
16
+ * `enum`) property, that property is the tag:
17
+ * - the value's tag picks a branch -> only that branch's errors are kept;
18
+ * - the tag is missing or unknown -> ONE error:
19
+ * "`type` must be one of noul, score, choice".
20
+ * A `oneOf` without a tag is left as Ajv reported it.
21
+ */
22
+ /** The subset of Ajv's `ErrorObject` this module reads. */
23
+ export interface SchemaErrorLike {
24
+ keyword: string;
25
+ instancePath: string;
26
+ schemaPath: string;
27
+ params: object;
28
+ message?: string;
29
+ propertyName?: string;
30
+ }
31
+ export interface SchemaViolation {
32
+ /** Dotted path inside the value (`criteria[1]`, `a.b`); "" for the root. */
33
+ path: string;
34
+ message: string;
35
+ }
36
+ /** Keyword of the single error that replaces a missing/unknown-tag `oneOf`. */
37
+ export declare const ONE_OF_TAG_KEYWORD = "oneOfTag";
38
+ /**
39
+ * Ajv `instancePath` (JSON pointer) -> dotted path:
40
+ * `/questions/tone/criteria/1` -> `questions.tone.criteria[1]`.
41
+ * Walks the value so only real array indexes become `[n]`.
42
+ */
43
+ export declare function instancePathToDotted(instancePath: string, value: unknown): string;
44
+ export interface OneOfTag {
45
+ /** The tag property name, e.g. `type`. */
46
+ name: string;
47
+ /** Tag value of each branch, in branch order. */
48
+ values: unknown[];
49
+ }
50
+ /**
51
+ * Find the tag of a `oneOf`: the ONE property that is a `const` (or a
52
+ * one-value `enum`) in every branch, with distinct values. Undefined when
53
+ * there is none, or more than one candidate.
54
+ */
55
+ export declare function findOneOfTag(branches: unknown[], root: unknown): OneOfTag | undefined;
56
+ /**
57
+ * Collapse tagged `oneOf` noise. Returns a new list in Ajv's order: errors
58
+ * from non-selected branches are dropped, and a missing/unknown tag becomes
59
+ * a single `oneOfTag` error at the union's position.
60
+ */
61
+ export declare function reshapeOneOfErrors<E extends SchemaErrorLike>(errors: readonly E[], rootSchema: unknown, value: unknown): (E | SchemaErrorLike)[];
62
+ /** One error -> message. Undefined means "skip it, a nested error says why". */
63
+ export declare function describeSchemaError(error: SchemaErrorLike): string | undefined;
64
+ /**
65
+ * Ajv errors for `value` against `rootSchema` -> violations, with tagged
66
+ * `oneOf` noise collapsed. Call it only when validation failed: it never
67
+ * returns an empty list.
68
+ */
69
+ export declare function schemaErrorsToViolations(errors: readonly SchemaErrorLike[], rootSchema: unknown, value: unknown): SchemaViolation[];