@cartanova/qgrid-cli 2.7.0 → 2.7.1

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.
Files changed (126) hide show
  1. package/bundle/dist/_virtual/_rolldown/runtime.js +20 -1
  2. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +17 -6
  3. package/bundle/dist/application/qgrid/qgrid.frame.js +1 -1
  4. package/bundle/dist/application/qgrid/schema-prompt.js +240 -0
  5. package/bundle/dist/application/qgrid/tool-emulation.js +7 -5
  6. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/2020.js +112 -0
  7. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js +140 -0
  8. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/index.js +676 -0
  9. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/scope.js +143 -0
  10. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/errors.js +104 -0
  11. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/index.js +225 -0
  12. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/names.js +30 -0
  13. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/ref_error.js +20 -0
  14. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/resolve.js +135 -0
  15. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/rules.js +63 -0
  16. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/util.js +148 -0
  17. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/validate/applicability.js +25 -0
  18. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/validate/boolSchema.js +50 -0
  19. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/validate/dataType.js +174 -0
  20. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/validate/defaults.js +33 -0
  21. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/validate/index.js +435 -0
  22. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/validate/keyword.js +115 -0
  23. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/validate/subschema.js +74 -0
  24. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/core.js +585 -0
  25. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/data.js +20 -0
  26. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/index.js +44 -0
  27. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/meta/applicator.js +52 -0
  28. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/meta/content.js +22 -0
  29. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/meta/core.js +53 -0
  30. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/meta/format-annotation.js +18 -0
  31. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/meta/meta-data.js +38 -0
  32. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/meta/unevaluated.js +21 -0
  33. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/meta/validation.js +87 -0
  34. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/refs/json-schema-2020-12/schema.js +61 -0
  35. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/runtime/equal.js +14 -0
  36. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/runtime/ucs2length.js +27 -0
  37. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/runtime/uri.js +14 -0
  38. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/runtime/validation_error.js +18 -0
  39. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/additionalItems.js +59 -0
  40. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/additionalProperties.js +98 -0
  41. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/allOf.js +31 -0
  42. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/anyOf.js +18 -0
  43. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/contains.js +91 -0
  44. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/dependencies.js +89 -0
  45. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/dependentSchemas.js +18 -0
  46. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/if.js +64 -0
  47. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/index.js +62 -0
  48. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/items.js +61 -0
  49. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/items2020.js +36 -0
  50. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/not.js +33 -0
  51. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/oneOf.js +53 -0
  52. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/patternProperties.js +63 -0
  53. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/prefixItems.js +19 -0
  54. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/properties.js +54 -0
  55. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/propertyNames.js +43 -0
  56. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/applicator/thenElse.js +19 -0
  57. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/code.js +132 -0
  58. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/core/id.js +16 -0
  59. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/core/index.js +24 -0
  60. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/core/ref.js +114 -0
  61. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/discriminator/index.js +103 -0
  62. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/discriminator/types.js +16 -0
  63. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/draft2020.js +37 -0
  64. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/dynamic/dynamicAnchor.js +48 -0
  65. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/dynamic/dynamicRef.js +47 -0
  66. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/dynamic/index.js +24 -0
  67. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/dynamic/recursiveAnchor.js +22 -0
  68. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/dynamic/recursiveRef.js +17 -0
  69. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/format/format.js +94 -0
  70. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/format/index.js +12 -0
  71. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/metadata.js +25 -0
  72. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/next.js +21 -0
  73. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/unevaluated/index.js +15 -0
  74. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedItems.js +48 -0
  75. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedProperties.js +61 -0
  76. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/const.js +29 -0
  77. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/dependentRequired.js +19 -0
  78. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/enum.js +51 -0
  79. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/index.js +50 -0
  80. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/limitContains.js +20 -0
  81. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/limitItems.js +30 -0
  82. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/limitLength.js +35 -0
  83. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/limitNumber.js +49 -0
  84. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/limitProperties.js +30 -0
  85. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/multipleOf.js +29 -0
  86. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/pattern.js +40 -0
  87. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/required.js +74 -0
  88. package/bundle/dist/node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/vocabularies/validation/uniqueItems.js +72 -0
  89. package/bundle/dist/node_modules/.pnpm/fast-deep-equal@3.1.3/node_modules/fast-deep-equal/index.js +35 -0
  90. package/bundle/dist/node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/index.js +238 -0
  91. package/bundle/dist/node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/schemes.js +189 -0
  92. package/bundle/dist/node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/utils.js +284 -0
  93. package/bundle/dist/node_modules/.pnpm/json-schema-traverse@1.0.0/node_modules/json-schema-traverse/index.js +80 -0
  94. package/bundle/dist/testing/schema-bench.js +0 -0
  95. package/bundle/dist/utils/providers/anthropic/fence-strip.js +83 -0
  96. package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +3 -5
  97. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +250 -34
  98. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +36 -5
  99. package/bundle/src/application/qgrid/qgrid.frame.test.ts +120 -153
  100. package/bundle/src/application/qgrid/qgrid.frame.ts +2 -0
  101. package/bundle/src/application/qgrid/schema-prompt.test.ts +166 -0
  102. package/bundle/src/application/qgrid/schema-prompt.ts +293 -0
  103. package/bundle/src/application/qgrid/tool-emulation.test.ts +36 -0
  104. package/bundle/src/application/qgrid/tool-emulation.ts +19 -7
  105. package/bundle/src/testing/schema-bench.test.ts +358 -0
  106. package/bundle/src/testing/schema-bench.ts +352 -0
  107. package/bundle/src/utils/providers/anthropic/fence-strip.test.ts +122 -0
  108. package/bundle/src/utils/providers/anthropic/fence-strip.ts +105 -0
  109. package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +2 -8
  110. package/bundle/web-dist/client/assets/{index-C24D5PiN.js → index-C0594sjh.js} +3 -3
  111. package/bundle/web-dist/client/assets/{logs-CIlZG6A3.js → logs-BotUcOEB.js} +1 -1
  112. package/bundle/web-dist/client/assets/{monit-DZFBMxKl.js → monit-kk3vnGvu.js} +1 -1
  113. package/bundle/web-dist/client/assets/{routes-C3wlSNBL.js → routes-J4bnHVfl.js} +1 -1
  114. package/bundle/web-dist/client/assets/{settings-ClRDWO-8.js → settings-CAAQSIyg.js} +1 -1
  115. package/bundle/web-dist/client/assets/{show-BP7U8yyJ.js → show-BryNvjUT.js} +1 -1
  116. package/bundle/web-dist/client/assets/{tokens-DfQeULnJ.js → tokens-DDrATjGA.js} +1 -1
  117. package/bundle/web-dist/client/assets/{use-oauth-login-flow-CY-ASCfL.js → use-oauth-login-flow-CptleCFM.js} +1 -1
  118. package/bundle/web-dist/client/index.html +1 -1
  119. package/bundle/web-dist/server/entry-server.generated.js +1 -1
  120. package/package.json +1 -1
  121. package/skills/qgrid/references/ai-sdk-provider-contract.md +20 -15
  122. package/skills/qgrid/references/anthropic-claude-code-runtime.md +31 -14
  123. package/skills/qgrid/references/decision-rationale.md +13 -3
  124. package/skills/qgrid/references/prompt-cache-and-usage.md +4 -1
  125. package/skills/qgrid/references/provider-runtime-differences.md +3 -3
  126. package/skills/qgrid/references/tool-calling-and-multiturn.md +16 -6
@@ -0,0 +1,293 @@
1
+ /**
2
+ * output schema / tool envelope 계약의 프롬프트 렌더링 (SON-532).
3
+ *
4
+ * anthropic 경로는 CC 에 --json-schema 를 전달하지 않는다 — 강제 없는 사후 채점(AJV)이
5
+ * 소비자 의도와 충돌해 내부 재시도 루프를 만들었기 때문. 대신 여기서 계약을 텍스트로
6
+ * 렌더해 system prompt 말미에 주입하고, 판정은 소비자 zod(스키마) / parseEnvelope
7
+ * (envelope, tool-emulation.ts)가 맡는다.
8
+ *
9
+ * 원칙:
10
+ * - user 스키마는 수신 원형 그대로 안내한다 — strictify(전 필드 required 강제,
11
+ * additionalProperties:false)를 거치지 않아 소비자의 nullish/optional 의도가 보존된다.
12
+ * - envelope 은 buildToolCallSchema(tool-emulation-schema.ts)처럼 user 스키마를 $defs 로
13
+ * 합성하지 않는다. 텍스트 안내라 `$ref` 문서 루트 충돌이 없어 rebase 가 불필요하고,
14
+ * 그 덕에 rebase 가 거절하던 형태(named anchor $ref 등)도 그대로 통과한다.
15
+ * - 렌더된 계약은 parseEnvelope 의 zod 계약({"result": ...} 한 겹, 상호배타 변형)과
16
+ * 정확히 일치해야 한다 — 교차 테스트(schema-prompt.test.ts)가 드리프트를 막는다.
17
+ */
18
+ import { type QgridTool, type QueryInput } from "./qgrid.types";
19
+
20
+ /**
21
+ * system prompt 말미에 계약을 덧붙인다. anthropic 은 cold-only(매 턴 CC 재스폰 + system
22
+ * 재전송)라 멀티턴에도 계약이 매 턴 일관되게 주입된다. system 이 argv 64KB 를 넘으면
23
+ * claude-session 의 --system-prompt-file 분기가 흡수한다.
24
+ */
25
+ export function composeSystemWithSchemaContract(
26
+ system: string | undefined,
27
+ input: Pick<QueryInput, "tools" | "jsonSchema">,
28
+ ): string | undefined {
29
+ const contract = renderSchemaContractPrompt(input);
30
+ if (contract === undefined) return system;
31
+ return system ? `${system}\n\n${contract}` : contract;
32
+ }
33
+
34
+ /** 요청에 스키마/tools 가 없으면 undefined — 계약 주입 없이 평범한 텍스트 요청이다. */
35
+ export function renderSchemaContractPrompt(
36
+ input: Pick<QueryInput, "tools" | "jsonSchema">,
37
+ ): string | undefined {
38
+ if (input.tools?.length) {
39
+ return renderToolEnvelopePrompt(input.tools, input.jsonSchema);
40
+ }
41
+ if (input.jsonSchema !== undefined) {
42
+ return renderOutputSchemaPrompt(input.jsonSchema);
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ // 1091748 실측(스키마 원문 + "펜스·설명 없이 JSON 만" 지시)에서 검증된 형식을 따른다.
48
+ export function renderOutputSchemaPrompt(jsonSchema: string): string {
49
+ return [
50
+ "## Output Format",
51
+ "",
52
+ "Your entire reply must be a single JSON object that conforms to the JSON Schema below.",
53
+ "Output raw JSON only — no code fences, no prose before or after the JSON.",
54
+ "Start your reply with { and end it with }.",
55
+ "",
56
+ "<output-json-schema>",
57
+ jsonSchema,
58
+ "</output-json-schema>",
59
+ ].join("\n");
60
+ }
61
+
62
+ /**
63
+ * envelope 예시 JSON. 렌더 문구와 parseEnvelope 계약의 드리프트를 교차 테스트로 잡기
64
+ * 위해 분리 export 한다 — 예시가 실제 파서를 통과하지 못하면 안내문이 틀린 것이다.
65
+ *
66
+ * 예시는 스스로 계약을 지켜야 한다: args 는 첫 tool 의 inputSchema 를 실제로 만족하는
67
+ * 인스턴스여야 한다. 생성기가 다루지 못하는 keyword(pattern/$ref/allOf 등)가 스키마에
68
+ * 있으면 **fail-closed** — 위반 가능성이 있는 예시를 싣는 대신 예시 자체를 생략하고
69
+ * 렌더러가 프로즈 서술로 대체한다 (`toolCallExample: undefined`).
70
+ */
71
+ export function buildEnvelopeExamples(tools: QgridTool[]): {
72
+ toolCallExample?: string;
73
+ textAnswerExample: string;
74
+ } {
75
+ const firstTool = tools[0];
76
+ const args = firstTool ? exampleValueForSchema(firstTool.inputSchema) : { ok: true, value: {} };
77
+ return {
78
+ ...(args.ok
79
+ ? {
80
+ toolCallExample: JSON.stringify({
81
+ result: {
82
+ action: "tool_call",
83
+ answer: null,
84
+ toolCalls: [
85
+ { toolName: firstTool?.name ?? "toolName", args: JSON.stringify(args.value) },
86
+ ],
87
+ },
88
+ }),
89
+ }
90
+ : {}),
91
+ textAnswerExample: JSON.stringify({
92
+ result: { action: "answer", answer: "your final answer", toolCalls: null },
93
+ }),
94
+ };
95
+ }
96
+
97
+ type ExampleResult = { ok: true; value: unknown } | { ok: false };
98
+
99
+ /** 생성기가 값 선택에 반영하는 keyword. 어노테이션(description 등)은 값과 무관해 무시한다. */
100
+ const EXAMPLE_ANNOTATION_KEYWORDS = new Set([
101
+ "description",
102
+ "title",
103
+ "default",
104
+ "examples",
105
+ "$comment",
106
+ "deprecated",
107
+ "readOnly",
108
+ "writeOnly",
109
+ ]);
110
+ const EXAMPLE_HANDLED_KEYWORDS = new Set([
111
+ "type",
112
+ "enum",
113
+ "const",
114
+ "properties",
115
+ "required",
116
+ "additionalProperties",
117
+ "items",
118
+ "minItems",
119
+ "maxItems",
120
+ "minLength",
121
+ "maxLength",
122
+ "minimum",
123
+ "maximum",
124
+ "exclusiveMinimum",
125
+ "exclusiveMaximum",
126
+ "anyOf",
127
+ "oneOf",
128
+ ]);
129
+
130
+ /**
131
+ * inputSchema 를 만족하는 예시 인스턴스 생성. 지원 keyword 집합 밖의 제약
132
+ * (pattern/format/$ref/allOf/not/if/multipleOf/uniqueItems/...)을 만나면 값을
133
+ * 지어내지 않고 { ok: false } 를 돌려준다 — 계약 위반 예시는 모델이 베낀다.
134
+ */
135
+ function exampleValueForSchema(schema: unknown): ExampleResult {
136
+ if (schema === true || schema === undefined) return { ok: true, value: {} };
137
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) return { ok: false };
138
+ const node = schema as Record<string, unknown>;
139
+
140
+ for (const keyword of Object.keys(node)) {
141
+ if (!EXAMPLE_HANDLED_KEYWORDS.has(keyword) && !EXAMPLE_ANNOTATION_KEYWORDS.has(keyword)) {
142
+ return { ok: false };
143
+ }
144
+ }
145
+
146
+ if (Array.isArray(node.enum) && node.enum.length > 0) return { ok: true, value: node.enum[0] };
147
+ if ("const" in node) return { ok: true, value: node.const };
148
+
149
+ for (const kw of ["anyOf", "oneOf"] as const) {
150
+ if (Array.isArray(node[kw])) {
151
+ // oneOf 는 "정확히 하나"라 첫 브랜치 값이 다른 브랜치에도 매칭될 수 있다 —
152
+ // 검증기 없이 확신할 수 없으므로 브랜치가 하나일 때만 생성한다.
153
+ const list = node[kw] as unknown[];
154
+ if (kw === "oneOf" && list.length > 1) return { ok: false };
155
+ for (const branch of list) {
156
+ const result = exampleValueForSchema(branch);
157
+ if (result.ok) return result;
158
+ }
159
+ return { ok: false };
160
+ }
161
+ }
162
+
163
+ const type = Array.isArray(node.type) ? node.type[0] : node.type;
164
+ switch (type) {
165
+ case "string": {
166
+ const min = typeof node.minLength === "number" ? node.minLength : 0;
167
+ if (min > 64) return { ok: false };
168
+ const base = "example";
169
+ const value = base.length >= min ? base : base.padEnd(min, "x");
170
+ if (typeof node.maxLength === "number" && value.length > node.maxLength) {
171
+ if (node.maxLength < min) return { ok: false };
172
+ return { ok: true, value: value.slice(0, node.maxLength) };
173
+ }
174
+ return { ok: true, value };
175
+ }
176
+ case "number":
177
+ case "integer": {
178
+ let value = 0;
179
+ if (typeof node.minimum === "number") value = node.minimum;
180
+ else if (typeof node.exclusiveMinimum === "number") value = node.exclusiveMinimum + 1;
181
+ if (typeof node.maximum === "number" && value > node.maximum) return { ok: false };
182
+ if (typeof node.exclusiveMaximum === "number" && value >= node.exclusiveMaximum) {
183
+ return { ok: false };
184
+ }
185
+ return { ok: true, value };
186
+ }
187
+ case "boolean":
188
+ return { ok: true, value: false };
189
+ case "null":
190
+ return { ok: true, value: null };
191
+ case "array": {
192
+ const min = typeof node.minItems === "number" ? node.minItems : 0;
193
+ if (min > 8) return { ok: false };
194
+ if (typeof node.maxItems === "number" && node.maxItems < min) return { ok: false };
195
+ if (min === 0) return { ok: true, value: [] };
196
+ const item = exampleValueForSchema(node.items);
197
+ if (!item.ok) return { ok: false };
198
+ // 같은 값 반복이라 uniqueItems 와 충돌하지만, uniqueItems 는 지원 집합 밖이라
199
+ // 위의 keyword 검사에서 이미 fail-closed 된다.
200
+ return { ok: true, value: Array.from({ length: min }, () => item.value) };
201
+ }
202
+ case undefined:
203
+ case "object": {
204
+ const properties =
205
+ node.properties && typeof node.properties === "object" && !Array.isArray(node.properties)
206
+ ? (node.properties as Record<string, unknown>)
207
+ : {};
208
+ const required = Array.isArray(node.required)
209
+ ? node.required.filter((k): k is string => typeof k === "string")
210
+ : [];
211
+ const entries: Array<[string, unknown]> = [];
212
+ for (const key of required) {
213
+ const child = exampleValueForSchema(properties[key]);
214
+ if (!child.ok) return { ok: false };
215
+ entries.push([key, child.value]);
216
+ }
217
+ return { ok: true, value: Object.fromEntries(entries) };
218
+ }
219
+ default:
220
+ return { ok: false };
221
+ }
222
+ }
223
+
224
+ // envelope 계약 문구는 buildToolCallSchema 의 description 들과 같은 표현을 쓴다
225
+ // ("Do not invoke ... as native Claude Code tools" 등) — 모델이 structured/프롬프트
226
+ // 어느 안내를 받아도 같은 규칙을 읽게 한다.
227
+ export function renderToolEnvelopePrompt(tools: QgridTool[], jsonSchema?: string): string {
228
+ const { toolCallExample, textAnswerExample } = buildEnvelopeExamples(tools);
229
+
230
+ const toolList = tools
231
+ .map((tool) => {
232
+ const description = tool.description ? ` — ${tool.description}` : "";
233
+ return `- ${tool.name}${description}\n inputSchema: ${JSON.stringify(tool.inputSchema)}`;
234
+ })
235
+ .join("\n");
236
+
237
+ // 예시가 raw-JSON-only 규칙을 스스로 어기면 안 된다: json answer 는 스키마 종속이라
238
+ // 유효한 리터럴 예시를 만들 수 없으므로 예시 없이 프로즈로만 서술한다 (자리표시자가
239
+ // 섞인 비 JSON 예시를 모델이 그대로 베끼는 것 방지).
240
+ const answerVariant =
241
+ jsonSchema === undefined
242
+ ? [
243
+ "2. Final answer — use only when no further client tool result is needed:",
244
+ textAnswerExample,
245
+ '- "toolCalls" must be null.',
246
+ '- "answer" must be a plain string with your final answer (not null).',
247
+ ]
248
+ : [
249
+ "2. Final answer — use only when no further client tool result is needed:",
250
+ '- Set "action" to "answer" and "toolCalls" to null.',
251
+ '- "answer" must be a JSON value (not null, and not a string of JSON) that conforms to the JSON Schema below.',
252
+ "",
253
+ "<answer-json-schema>",
254
+ jsonSchema,
255
+ "</answer-json-schema>",
256
+ ];
257
+
258
+ // 예시 생성기가 다루지 못하는 inputSchema(fail-closed)면 구조 서술로 대체한다 —
259
+ // 스키마를 위반할 수 있는 예시를 싣는 것보다 예시가 없는 편이 안전하다.
260
+ const toolCallVariant = [
261
+ toolCallExample !== undefined
262
+ ? "1. Request client-side tool execution, for example:"
263
+ : "1. Request client-side tool execution:",
264
+ ...(toolCallExample !== undefined
265
+ ? [toolCallExample]
266
+ : [
267
+ '- Set "action" to "tool_call" and "answer" to null.',
268
+ '- "toolCalls" is an array of objects, each {"toolName": ..., "args": ...}.',
269
+ ]),
270
+ '- "toolCalls" must contain at least one entry.',
271
+ '- "toolName" must be one of the client tool names listed below.',
272
+ '- "args" must be the tool arguments encoded as a JSON string (not an object), conforming to that tool\'s inputSchema.',
273
+ '- "answer" must be null.',
274
+ ];
275
+
276
+ return [
277
+ "## Client Tool Protocol",
278
+ "",
279
+ "The client-side tools listed below are executed by the client, which returns each result",
280
+ "in a follow-up turn. Do not invoke them as native Claude Code tools.",
281
+ "",
282
+ 'Your entire reply must be a single JSON object of the form {"result": ...} —',
283
+ "raw JSON only, no code fences, no prose before or after the JSON.",
284
+ 'The "result" value is exactly one of the two variants:',
285
+ "",
286
+ ...toolCallVariant,
287
+ "",
288
+ ...answerVariant,
289
+ "",
290
+ "Available client tools:",
291
+ toolList,
292
+ ].join("\n");
293
+ }
@@ -161,6 +161,42 @@ describe("applyToolCallEmulation envelope validation", () => {
161
161
  expect(() => applyEnvelope(result, "text")).toThrowError(ToolCallEmulationError);
162
162
  });
163
163
 
164
+ // 렌더 문구와 parseEnvelope 계약의 드리프트 방지(SON-532): schema-prompt 안내문에 실린
165
+ // 예시가 실제 공개 진입점을 통과해야 한다. 예시가 실패하면 안내문이 틀린 것이다.
166
+ // (schema-prompt.test.ts 가 아닌 여기 두는 이유: 이 파일은 이미 실모듈을 로드한다 —
167
+ // isolate:false 공유 레지스트리에서 mock 파일 오염을 피한다.)
168
+ it("schema-prompt 예시 ↔ envelope 교차 계약: tool_call 예시가 해석된다", async () => {
169
+ const { buildEnvelopeExamples } = await import("./schema-prompt");
170
+ const { toolCallExample, textAnswerExample } = buildEnvelopeExamples(tools);
171
+ expect(toolCallExample).toBeDefined(); // lookup inputSchema 는 생성 가능 집합 안이다
172
+
173
+ for (const answerKind of ["text", "json"] as const) {
174
+ const out = applyToolCallEmulation({ ...baseResult, text: toolCallExample! }, tools, {
175
+ answerKind,
176
+ });
177
+ expect(out.finishReason).toBe("tool-calls");
178
+ expect(out.content[0]).toMatchObject({ type: "tool-call", toolName: "lookup" });
179
+ }
180
+
181
+ const answered = applyToolCallEmulation({ ...baseResult, text: textAnswerExample }, tools, {
182
+ answerKind: "text",
183
+ });
184
+ expect(answered.finishReason).toBe("stop");
185
+ expect(answered.text).toBe("your final answer");
186
+ });
187
+
188
+ // SON-532: anthropic 은 envelope 을 프롬프트로만 안내하므로 프로즈 응답이 "예상 가능한
189
+ // 모델 실패"가 됐다. 그래도 구제하지 않는다 — 정직한 실패가 소비자 재시도를 안내한다.
190
+ it("rejects a prose response with a message that guides the caller", () => {
191
+ expect(() =>
192
+ applyToolCallEmulation(
193
+ { ...baseResult, text: "Sure! I looked it up and the answer is 42." },
194
+ tools,
195
+ { answerKind: "json" },
196
+ ),
197
+ ).toThrow(/response is not JSON.*retry is the caller's decision/);
198
+ });
199
+
164
200
  it("rejects an envelope missing the result wrapper", () => {
165
201
  expect(() =>
166
202
  applyToolCallEmulation(
@@ -119,11 +119,18 @@ function parseEnvelope(text: string, tools: QgridTool[], answerKind: AnswerKind)
119
119
  try {
120
120
  value = JSON.parse(text);
121
121
  } catch (error) {
122
- // 관용 폴백 없음: envelope 는 양 provider 의 structured output 으로 강제되는 내부
123
- // 계약이라 여기 도달한 비 JSON 은 버그다. 과거의 텍스트 구제는 퇴화 봉투를 답변으로
124
- // 흘려보내 조용한 오염을 만들었다(2026-07 medpath, 13.5k 건).
125
- logger.warn(`tool envelope parse failed: ${(error as Error).message}`);
126
- throw new ToolCallEmulationError(`invalid tool envelope: ${(error as Error).message}`);
122
+ // 관용 폴백 없음. openai 는 envelope 이 structured output 으로 강제되므로 여기 도달한
123
+ // 비 JSON 은 버그다. anthropic 은 계약을 프롬프트로만 안내하므로(SON-532) 비 JSON 이
124
+ // "예상 가능한 모델 실패"지만, 그래도 구제하지 않는다 — 과거의 텍스트 구제는 퇴화
125
+ // 봉투를 답변으로 흘려보내 조용한 오염을 만들었다(2026-07 medpath, 13.5k 건).
126
+ // 정직한 실패가 보여야 소비자가 재시도하거나 근본 원인을 고친다.
127
+ // 진단에는 위반 원문의 머리가 필요하다(SON-495 전례) — parse 에러 위치만으로는
128
+ // "프로즈로 답했는지 / 펜스가 남았는지 / JSON 이 잘렸는지"를 구분할 수 없다.
129
+ const head = JSON.stringify(text.slice(0, 200));
130
+ logger.warn(`tool envelope parse failed: ${(error as Error).message}; head=${head}`);
131
+ throw new ToolCallEmulationError(
132
+ `invalid tool envelope (response is not JSON — the model ignored the envelope contract; retry is the caller's decision): ${(error as Error).message}; response head: ${head}`,
133
+ );
127
134
  }
128
135
 
129
136
  assertStructuredEnvelopeComplexity(value);
@@ -133,8 +140,13 @@ function parseEnvelope(text: string, tools: QgridTool[], answerKind: AnswerKind)
133
140
  const issue = validation.error.issues[0];
134
141
  const path = issue && issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
135
142
  const detail = issue?.message ?? "unknown validation issue";
136
- logger.warn(`tool envelope validation failed${path}: ${detail}`);
137
- throw new ToolCallEmulationError(`invalid tool envelope${path}: ${detail}`);
143
+ // valid JSON 이지만 계약 위반인 경우에도 원문 머리를 남긴다 — zod 경로만으로는
144
+ // "키 생략인지 / 래퍼로 감쌌는지 / 타입이 틀렸는지"의 실물을 볼 수 없다.
145
+ const head = JSON.stringify(text.slice(0, 200));
146
+ logger.warn(`tool envelope validation failed${path}: ${detail}; head=${head}`);
147
+ throw new ToolCallEmulationError(
148
+ `invalid tool envelope${path}: ${detail}; response head: ${head}`,
149
+ );
138
150
  }
139
151
 
140
152
  // Zod clones JSON objects and drops own "__proto__" keys. The parsed JSON is safe to return