@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
@@ -12,6 +12,7 @@ import { InternalServerErrorException, ServiceUnavailableException } from "sonam
12
12
  import { type JsonValue } from "../../codex-protocol/serde_json/JsonValue";
13
13
  import { SD } from "../../i18n/sd.generated";
14
14
  import { type AnthropicDispatcher } from "../../utils/providers/anthropic/anthropic-dispatcher";
15
+ import { createFenceStripTransform } from "../../utils/providers/anthropic/fence-strip";
15
16
  import { getAccessToken } from "../../utils/providers/common/credentials";
16
17
  import { calculateCostUsd } from "../../utils/providers/common/model-cost";
17
18
  import {
@@ -34,6 +35,7 @@ import {
34
35
  type QueryOutput,
35
36
  type TokenStats,
36
37
  } from "./qgrid.types";
38
+ import { composeSystemWithSchemaContract } from "./schema-prompt";
37
39
  import { type TokenSubscriber } from "./token-subscriber";
38
40
  import { applyToolCallEmulation } from "./tool-emulation";
39
41
  import { buildToolCallSchema } from "./tool-emulation-schema";
@@ -149,8 +151,10 @@ export class QgridDispatcherClass {
149
151
  const result = await this.anthropicDispatcher.generate({
150
152
  // AnthropicDispatcher 내부에서 알아서 provider prefix 를 canonical model 로 정규화함
151
153
  model: input.model,
152
- systemPrompt: input.system,
153
- outputSchema,
154
+ // 스키마/envelope 계약은 --json-schema 대신 system 말미의 텍스트로 안내한다(SON-532).
155
+ // outputSchema 는 전달하지 않는다 — anthropic 에서 항상 undefined(U1)이며,
156
+ // CC 로 가는 --json-schema 채널 자체가 닫혀 있음을 여기서 명시한다.
157
+ systemPrompt: composeSystemWithSchemaContract(input.system, input),
154
158
  effort: input.effort,
155
159
  timeoutMs: input.timeout,
156
160
  abortSignal,
@@ -220,12 +224,24 @@ export class QgridDispatcherClass {
220
224
  } else if (route.provider === "anthropic") {
221
225
  if (!this.anthropicDispatcher) throw this.notReadyError("anthropic");
222
226
 
227
+ // 계약(스키마/envelope)이 주입된 요청의 델타에서 코드펜스를 벗긴다(SON-532).
228
+ // 비스트림 최종 텍스트는 stream-json-adapter 의 stripFences 가 같은 시맨틱으로
229
+ // 처리하므로 델타 연결과 done.text 가 어긋나지 않는다. 일반 텍스트 요청은 무변경.
230
+ const hasSchemaContract = input.jsonSchema !== undefined || Boolean(input.tools?.length);
231
+ const fenceStrip = hasSchemaContract ? createFenceStripTransform() : undefined;
232
+ const emitDelta = fenceStrip
233
+ ? (text: string) => {
234
+ const safe = fenceStrip.push(text);
235
+ if (safe) cb.onDelta(safe);
236
+ }
237
+ : cb.onDelta;
238
+
223
239
  const decision = decideConvRouting(input);
224
240
  await this.anthropicDispatcher.generateStream(
225
241
  {
226
242
  model: input.model,
227
- systemPrompt: input.system,
228
- outputSchema,
243
+ // 스키마/envelope 계약은 --json-schema 대신 system 말미의 텍스트로 안내한다(SON-532)
244
+ systemPrompt: composeSystemWithSchemaContract(input.system, input),
229
245
  effort: input.effort,
230
246
  coldInput: decision.coldInput,
231
247
  coldHistory: decision.coldHistory,
@@ -235,9 +251,14 @@ export class QgridDispatcherClass {
235
251
  imageGenerationOptions: input.imageGenerationOptions,
236
252
  },
237
253
  {
238
- onDelta: cb.onDelta,
254
+ onDelta: emitDelta,
239
255
  onThreadId: cb.onThreadId,
240
256
  onComplete: (turnResult) => {
257
+ // 홀드백 잔여(닫는 펜스가 아니었던 tail)를 done 전에 마저 방출한다 —
258
+ // 델타를 조립하는 클라이언트(EnvelopeStreamParser 등)의 무손실 보장.
259
+ const rest = fenceStrip?.flush();
260
+ if (rest) cb.onDelta(rest);
261
+
241
262
  const issuedCoord = issueConvContext(turnResult.threadCoord, decision);
242
263
  cb.onComplete(
243
264
  applyToolCallEmulation(toEmulationResult(turnResult), input.tools, {
@@ -294,6 +315,16 @@ export function buildAndValidateStrictOutputSchema(
294
315
  input: Pick<QueryInput, "model" | "tools" | "jsonSchema">,
295
316
  provider = parseProviderRoute(input.model).provider,
296
317
  ): JsonValue | undefined {
318
+ // anthropic(SON-532): CC 에 --json-schema 를 전달하지 않는다 — 강제 없는 사후 채점이
319
+ // 소비자 의도(nullish 생략 등)와 충돌해 CC 내부 재시도 루프를 발화시켰다(왕복 2~4회,
320
+ // 4.7~9.1배 출력 청구 실측). 스키마/envelope 계약은 프롬프트 텍스트로 안내하고 판정은
321
+ // 소비자(zod)/parseEnvelope 가 맡는다. 따라서 strict 변환·argv 64KiB 제한 없이 수신
322
+ // 원형의 구문·복잡도(전역 512KiB 한도)만 검증해 caller-fault 를 조기에 돌려준다.
323
+ if (provider === "anthropic") {
324
+ parseAndValidateCallerSchemas(input);
325
+ return undefined;
326
+ }
327
+
297
328
  const outputSchema = buildStrictOutputSchema(input, provider);
298
329
  serializeAndValidateDispatchSchema(outputSchema, provider);
299
330
  return outputSchema;
@@ -22,23 +22,29 @@ const {
22
22
  finishRunWithErrorMock,
23
23
  finishRunAbortedMock,
24
24
  getRateLimitsByTokenIdMock,
25
- } = vi.hoisted(() => ({
26
- findOneMock: vi.fn(),
27
- findManyMock: vi.fn(),
28
- saveMock: vi.fn(),
29
- updateFieldsMock: vi.fn(),
30
- requestLogSaveMock: vi.fn(),
31
- requestLogCreateRunMock: vi.fn(),
32
- appendStepMock: vi.fn(),
33
- dispatcherQueryMock: vi.fn(),
34
- dispatcherQueryStreamMock: vi.fn(),
35
- beforeQueryMock: vi.fn(),
36
- afterQueryMock: vi.fn(),
37
- assertNativeRunAdmissionMock: vi.fn(),
38
- finishRunWithErrorMock: vi.fn(),
39
- finishRunAbortedMock: vi.fn(),
40
- getRateLimitsByTokenIdMock: vi.fn(),
41
- }));
25
+ } = vi.hoisted(() => {
26
+ // isolate:false 공유 레지스트리 방어: 같은 fork 에서 앞선 파일이 request-log/token/
27
+ // run-lifecycle 실모듈을 캐시해 두면 아래 vi.mock 들이 조용히 무시된다(실 DB 호출로
28
+ // 폭발). import 보다 먼저 레지스트리를 비워 이 파일은 항상 mock 그래프로 시작한다.
29
+ vi.resetModules();
30
+ return {
31
+ findOneMock: vi.fn(),
32
+ findManyMock: vi.fn(),
33
+ saveMock: vi.fn(),
34
+ updateFieldsMock: vi.fn(),
35
+ requestLogSaveMock: vi.fn(),
36
+ requestLogCreateRunMock: vi.fn(),
37
+ appendStepMock: vi.fn(),
38
+ dispatcherQueryMock: vi.fn(),
39
+ dispatcherQueryStreamMock: vi.fn(),
40
+ beforeQueryMock: vi.fn(),
41
+ afterQueryMock: vi.fn(),
42
+ assertNativeRunAdmissionMock: vi.fn(),
43
+ finishRunWithErrorMock: vi.fn(),
44
+ finishRunAbortedMock: vi.fn(),
45
+ getRateLimitsByTokenIdMock: vi.fn(),
46
+ };
47
+ });
42
48
 
43
49
  vi.mock("../request-log/request-log.model", () => ({
44
50
  MICRO_USD: 1_000_000,
@@ -467,56 +473,62 @@ describe("QgridFrame.query request logging", () => {
467
473
  expect(dispatcherQueryMock).not.toHaveBeenCalled();
468
474
  });
469
475
 
470
- it("returns 400 for an Anthropic positional tuple before logging or dispatch", async () => {
471
- await expect(
472
- QgridFrame.query({
473
- prompt: "hi",
474
- model: "anthropic/claude-opus-4-8",
475
- jsonSchema: JSON.stringify({
476
- type: "object",
477
- properties: {
478
- tuple: {
479
- type: "array",
480
- items: [{ type: "string" }, { type: "integer" }],
481
- },
476
+ // SON-532: anthropic 은 strictify 를 거치지 않으므로 strict 변환이 다루지 못하던 형태
477
+ // (positional tuple 등)도 거절 사유가 아니다 — 원형 그대로 프롬프트에 안내된다.
478
+ it.each([
479
+ [
480
+ "positional tuple",
481
+ {
482
+ type: "object",
483
+ properties: {
484
+ tuple: { type: "array", items: [{ type: "string" }, { type: "integer" }] },
485
+ },
486
+ required: ["tuple"],
487
+ },
488
+ ],
489
+ [
490
+ "tuple nested under not",
491
+ { type: "object", not: { type: "array", items: [{ type: "string" }] } },
492
+ ],
493
+ [
494
+ "prefixItems tuple with missing type",
495
+ {
496
+ type: "object",
497
+ properties: { tuple: { prefixItems: [{ type: "string" }, { type: "integer" }] } },
498
+ required: ["tuple"],
499
+ },
500
+ ],
501
+ [
502
+ "prefixItems tuple with nullable union type",
503
+ {
504
+ type: "object",
505
+ properties: {
506
+ tuple: {
507
+ type: ["array", "null"],
508
+ prefixItems: [{ type: "string" }, { type: "integer" }],
482
509
  },
483
- required: ["tuple"],
484
- }),
485
- }),
486
- ).rejects.toMatchObject({
487
- statusCode: 400,
488
- message: expect.stringContaining(
489
- "positional tuple schemas are not supported on Anthropic",
490
- ),
491
- });
492
-
493
- expect(beforeQueryMock).not.toHaveBeenCalled();
494
- expect(dispatcherQueryMock).not.toHaveBeenCalled();
495
- });
510
+ },
511
+ required: ["tuple"],
512
+ },
513
+ ],
514
+ ] as const)(
515
+ "accepts an Anthropic schema with %s and proceeds to dispatch (SON-532)",
516
+ async (_label, schema) => {
517
+ beforeQueryMock.mockResolvedValueOnce({ requestLogId: 41, stepIndex: 0 });
518
+ dispatcherQueryMock.mockResolvedValueOnce(queryOutput());
519
+ afterQueryMock.mockResolvedValueOnce({});
496
520
 
497
- it("returns 400 for an Anthropic tuple nested under not before logging or dispatch", async () => {
498
- await expect(
499
- QgridFrame.query({
500
- prompt: "hi",
501
- model: "anthropic/claude-opus-4-8",
502
- jsonSchema: JSON.stringify({
503
- type: "object",
504
- not: {
505
- type: "array",
506
- items: [{ type: "string" }],
507
- },
521
+ await expect(
522
+ QgridFrame.query({
523
+ prompt: "hi",
524
+ model: "anthropic/claude-opus-4-8",
525
+ jsonSchema: JSON.stringify(schema),
508
526
  }),
509
- }),
510
- ).rejects.toMatchObject({
511
- statusCode: 400,
512
- message: expect.stringContaining(
513
- "positional tuple schemas are not supported on Anthropic",
514
- ),
515
- });
527
+ ).resolves.toMatchObject({ text: "hello" });
516
528
 
517
- expect(beforeQueryMock).not.toHaveBeenCalled();
518
- expect(dispatcherQueryMock).not.toHaveBeenCalled();
519
- });
529
+ expect(dispatcherQueryMock).toHaveBeenCalledOnce();
530
+ },
531
+ );
520
532
 
521
533
  it("returns 400 for an OpenAI tuple under not instead of changing negative semantics", async () => {
522
534
  await expect(
@@ -654,40 +666,13 @@ describe("QgridFrame.query request logging", () => {
654
666
  },
655
667
  );
656
668
 
657
- it.each([
658
- ["missing", undefined],
659
- ["nullable union", ["array", "null"]],
660
- ] as const)(
661
- "returns 400 for an Anthropic positional tuple with %s type before logging or dispatch",
662
- async (_label, type) => {
663
- await expect(
664
- QgridFrame.query({
665
- prompt: "hi",
666
- model: "anthropic/claude-opus-4-8",
667
- jsonSchema: JSON.stringify({
668
- type: "object",
669
- properties: {
670
- tuple: {
671
- ...(type === undefined ? {} : { type }),
672
- prefixItems: [{ type: "string" }, { type: "integer" }],
673
- },
674
- },
675
- required: ["tuple"],
676
- }),
677
- }),
678
- ).rejects.toMatchObject({
679
- statusCode: 400,
680
- message: expect.stringContaining(
681
- "positional tuple schemas are not supported on Anthropic",
682
- ),
683
- });
684
-
685
- expect(beforeQueryMock).not.toHaveBeenCalled();
686
- expect(dispatcherQueryMock).not.toHaveBeenCalled();
687
- },
688
- );
669
+ // SON-532: argv 64KiB 제한은 --json-schema 전용이었다. 프롬프트 전달은 system 크기
670
+ // 분기(--system-prompt-file)가 흡수하므로 전역 복잡도 한도만 남는다.
671
+ it("accepts an Anthropic schema that exceeds the old argv size limit (SON-532)", async () => {
672
+ beforeQueryMock.mockResolvedValueOnce({ requestLogId: 41, stepIndex: 0 });
673
+ dispatcherQueryMock.mockResolvedValueOnce(queryOutput());
674
+ afterQueryMock.mockResolvedValueOnce({});
689
675
 
690
- it("returns 400 when the final Anthropic schema exceeds the safe argv size", async () => {
691
676
  await expect(
692
677
  QgridFrame.query({
693
678
  prompt: "hi",
@@ -704,13 +689,9 @@ describe("QgridFrame.query request logging", () => {
704
689
  properties: { answer: { type: "string" } },
705
690
  }),
706
691
  }),
707
- ).rejects.toMatchObject({
708
- statusCode: 400,
709
- message: expect.stringContaining("Anthropic dispatch schema exceeds argv"),
710
- });
692
+ ).resolves.toMatchObject({ text: "hello" });
711
693
 
712
- expect(beforeQueryMock).not.toHaveBeenCalled();
713
- expect(dispatcherQueryMock).not.toHaveBeenCalled();
694
+ expect(dispatcherQueryMock).toHaveBeenCalledOnce();
714
695
  });
715
696
 
716
697
  it("accepts logger: false in the wire schema", () => {
@@ -757,30 +738,22 @@ describe("QgridFrame.prepareStream", () => {
757
738
  });
758
739
  });
759
740
 
760
- it("returns 400 for an Anthropic tuple under patternProperties before allocating a stream id", async () => {
761
- await expect(
762
- QgridFrame.prepareStream({
763
- prompt: "hi",
764
- model: "anthropic/claude-opus-4-8",
765
- jsonSchema: JSON.stringify({
766
- type: "object",
767
- patternProperties: {
768
- "^tuple$": {
769
- type: "array",
770
- prefixItems: [{ type: "string" }],
771
- },
741
+ it("accepts an Anthropic tuple under patternProperties and allocates a stream id (SON-532)", async () => {
742
+ const { streamId } = await QgridFrame.prepareStream({
743
+ prompt: "hi",
744
+ model: "anthropic/claude-opus-4-8",
745
+ jsonSchema: JSON.stringify({
746
+ type: "object",
747
+ patternProperties: {
748
+ "^tuple$": {
749
+ type: "array",
750
+ prefixItems: [{ type: "string" }],
772
751
  },
773
- }),
752
+ },
774
753
  }),
775
- ).rejects.toMatchObject({
776
- statusCode: 400,
777
- message: expect.stringContaining(
778
- "positional tuple schemas are not supported on Anthropic",
779
- ),
780
754
  });
781
755
 
782
- expect(requestLogSaveMock).not.toHaveBeenCalled();
783
- expect(dispatcherQueryMock).not.toHaveBeenCalled();
756
+ expect(streamId).toEqual(expect.any(String));
784
757
  });
785
758
 
786
759
  it("returns 400 for a deeply nested tool schema before allocating a stream id", async () => {
@@ -796,42 +769,36 @@ describe("QgridFrame.prepareStream", () => {
796
769
  });
797
770
  });
798
771
 
799
- it("returns 400 for an unsupported composed-schema reference before allocating a stream id", async () => {
800
- await expect(
801
- QgridFrame.prepareStream({
802
- prompt: "hi",
803
- model: "anthropic/claude-sonnet-4-6",
804
- tools: [{ name: "lookup", inputSchema: { type: "object" } }],
805
- jsonSchema: JSON.stringify({
806
- type: "object",
807
- properties: { answer: { $ref: "#named-anchor" } },
808
- }),
772
+ // strict 합성 시절엔 $defs rebase 때문에 named anchor $ref 를 거절해야 했다.
773
+ // 프롬프트 전달은 원문을 그대로 안내하므로 재작성이 없고 거절 사유도 사라진다(SON-532).
774
+ it("accepts a named-anchor $ref on Anthropic and allocates a stream id (SON-532)", async () => {
775
+ const { streamId } = await QgridFrame.prepareStream({
776
+ prompt: "hi",
777
+ model: "anthropic/claude-sonnet-4-6",
778
+ tools: [{ name: "lookup", inputSchema: { type: "object" } }],
779
+ jsonSchema: JSON.stringify({
780
+ type: "object",
781
+ properties: { answer: { $ref: "#named-anchor" } },
809
782
  }),
810
- ).rejects.toMatchObject({
811
- statusCode: 400,
812
- message: expect.stringContaining(
813
- "only root-relative JSON Pointer $ref values are supported",
814
- ),
815
783
  });
784
+
785
+ expect(streamId).toEqual(expect.any(String));
816
786
  });
817
787
 
818
- it("returns 400 for an oversized Anthropic argv schema before allocating a stream id", async () => {
819
- await expect(
820
- QgridFrame.prepareStream({
821
- prompt: "hi",
822
- model: "anthropic/claude-sonnet-4-6",
823
- tools: [
824
- {
825
- name: "lookup",
826
- description: "x".repeat(64 * 1024),
827
- inputSchema: { type: "object" },
828
- },
829
- ],
830
- }),
831
- ).rejects.toMatchObject({
832
- statusCode: 400,
833
- message: expect.stringContaining("Anthropic dispatch schema exceeds argv"),
788
+ it("accepts an Anthropic schema beyond the old argv limit and allocates a stream id (SON-532)", async () => {
789
+ const { streamId } = await QgridFrame.prepareStream({
790
+ prompt: "hi",
791
+ model: "anthropic/claude-sonnet-4-6",
792
+ tools: [
793
+ {
794
+ name: "lookup",
795
+ description: "x".repeat(64 * 1024),
796
+ inputSchema: { type: "object" },
797
+ },
798
+ ],
834
799
  });
800
+
801
+ expect(streamId).toEqual(expect.any(String));
835
802
  });
836
803
  });
837
804
 
@@ -138,6 +138,8 @@ function rejectImageGenerationStream(args: QueryInput): void {
138
138
  );
139
139
  }
140
140
 
141
+ // anthropic 은 --json-schema 를 쓰지 않으므로(SON-532) strict 변환·argv 제한 없이 원형의
142
+ // 구문·복잡도만 검증된다 — provider 분기는 buildAndValidateStrictOutputSchema 가 소유한다.
141
143
  function rejectInvalidCallerSchemas(args: QueryInput): void {
142
144
  if (args.jsonSchema === undefined && !args.tools?.length) return;
143
145
 
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ buildEnvelopeExamples,
5
+ renderOutputSchemaPrompt,
6
+ renderSchemaContractPrompt,
7
+ renderToolEnvelopePrompt,
8
+ } from "./schema-prompt";
9
+
10
+ const TOOLS = [
11
+ {
12
+ name: "lookup",
13
+ description: "Look up a value",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: { key: { type: "string" } },
17
+ required: ["key"],
18
+ },
19
+ },
20
+ { name: "save", inputSchema: { type: "object" } },
21
+ ];
22
+
23
+ // 소비자가 nullish 로 선언한 필드가 있는 원형 — strictify 를 거치면 required 로 굳는 형태.
24
+ const USER_SCHEMA = JSON.stringify({
25
+ type: "object",
26
+ properties: {
27
+ title: { type: "string" },
28
+ mood: { type: ["string", "null"], description: "생략 가능" },
29
+ },
30
+ required: ["title"],
31
+ });
32
+
33
+ describe("renderOutputSchemaPrompt", () => {
34
+ it("스키마 원문을 재작성 없이 포함하고 raw JSON 지시를 담는다", () => {
35
+ const rendered = renderOutputSchemaPrompt(USER_SCHEMA);
36
+
37
+ expect(rendered).toContain(USER_SCHEMA);
38
+ expect(rendered).toContain("no code fences");
39
+ // strictify 흔적이 없어야 한다 — 원형에 없던 additionalProperties 강제가 생기면 안 된다.
40
+ expect(rendered).not.toContain("additionalProperties");
41
+ });
42
+
43
+ it("$ref 를 포함한 스키마도 원문 그대로 둔다", () => {
44
+ const schema = JSON.stringify({
45
+ type: "object",
46
+ properties: { answer: { $ref: "#named-anchor" } },
47
+ });
48
+
49
+ expect(renderOutputSchemaPrompt(schema)).toContain('"$ref":"#named-anchor"');
50
+ });
51
+ });
52
+
53
+ describe("renderToolEnvelopePrompt", () => {
54
+ it("envelope 계약({\"result\": ...} 래퍼·상호배타·native tool 금지)을 안내한다", () => {
55
+ const rendered = renderToolEnvelopePrompt(TOOLS);
56
+
57
+ expect(rendered).toContain('{"result": ...}');
58
+ expect(rendered).toContain("Do not invoke them as native Claude Code tools");
59
+ expect(rendered).toContain('"answer" must be null');
60
+ expect(rendered).toContain('"toolCalls" must be null');
61
+ expect(rendered).toContain("at least one entry");
62
+ // tool 목록과 inputSchema 원문
63
+ expect(rendered).toContain("- lookup — Look up a value");
64
+ expect(rendered).toContain('"required":["key"]');
65
+ expect(rendered).toContain("- save");
66
+ });
67
+
68
+ it("tools+스키마면 answer 자리에 user 스키마를 원형 그대로 안내한다", () => {
69
+ const rendered = renderToolEnvelopePrompt(TOOLS, USER_SCHEMA);
70
+
71
+ expect(rendered).toContain("<answer-json-schema>");
72
+ expect(rendered).toContain(USER_SCHEMA);
73
+ expect(rendered).toContain("conforms to the JSON Schema below");
74
+ // raw-JSON-only 규칙을 예시가 스스로 어기면 안 된다 — 자리표시자 섞인 비 JSON 예시 금지
75
+ expect(rendered).not.toContain("<JSON conforming");
76
+ });
77
+
78
+ it("생성기가 다루지 못하는 inputSchema 는 예시를 생략하고 구조 서술로 대체한다 (fail-closed)", () => {
79
+ const patternTool = [
80
+ {
81
+ name: "search",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: { query: { type: "string", pattern: "^[a-z]+$" } },
85
+ required: ["query"],
86
+ },
87
+ },
88
+ ];
89
+
90
+ // pattern 은 지원 집합 밖 — 위반 가능성이 있는 예시를 싣지 않는다
91
+ expect(buildEnvelopeExamples(patternTool).toolCallExample).toBeUndefined();
92
+ const rendered = renderToolEnvelopePrompt(patternTool);
93
+ expect(rendered).toContain('"toolCalls" is an array of objects');
94
+ expect(rendered).not.toContain('"args": "{'); // 리터럴 예시 부재
95
+ });
96
+
97
+ it("minLength·numeric bound·enum 을 반영한 예시가 실제 스키마를 만족한다 (ajv 교차검증)", async () => {
98
+ const { default: Ajv2020 } = await import("ajv/dist/2020");
99
+ const inputSchema = {
100
+ type: "object",
101
+ properties: {
102
+ key: { type: "string", minLength: 10 },
103
+ limit: { type: "integer", minimum: 5, maximum: 20 },
104
+ kind: { type: "string", enum: ["book", "film"] },
105
+ tags: { type: "array", items: { type: "string" }, minItems: 2 },
106
+ },
107
+ required: ["key", "limit", "kind", "tags"],
108
+ };
109
+ const { toolCallExample } = buildEnvelopeExamples([{ name: "lookup", inputSchema }]);
110
+ expect(toolCallExample).toBeDefined();
111
+
112
+ const envelope = JSON.parse(toolCallExample!) as {
113
+ result: { toolCalls: Array<{ args: string }> };
114
+ };
115
+ const args = JSON.parse(envelope.result.toolCalls[0]!.args) as unknown;
116
+ const validate = new Ajv2020({ strict: false }).compile(inputSchema);
117
+ expect(validate(args), JSON.stringify(validate.errors)).toBe(true);
118
+ });
119
+
120
+ it("tool_call 예시의 args 가 그 tool 의 required 필드를 채운 유효 JSON 이다", () => {
121
+ const { toolCallExample } = buildEnvelopeExamples(TOOLS);
122
+ expect(toolCallExample).toBeDefined();
123
+ const envelope = JSON.parse(toolCallExample!) as {
124
+ result: { toolCalls: Array<{ toolName: string; args: string }> };
125
+ };
126
+ const call = envelope.result.toolCalls[0]!;
127
+
128
+ expect(call.toolName).toBe("lookup");
129
+ const args = JSON.parse(call.args) as Record<string, unknown>;
130
+ // lookup inputSchema: required ["key"] (string)
131
+ expect(typeof args.key).toBe("string");
132
+ });
133
+
134
+ it("tools 만이면 answer 는 평문 string 안내다", () => {
135
+ const rendered = renderToolEnvelopePrompt(TOOLS);
136
+
137
+ expect(rendered).toContain("plain string");
138
+ expect(rendered).not.toContain("<answer-json-schema>");
139
+ });
140
+ });
141
+
142
+ describe("renderSchemaContractPrompt", () => {
143
+ it("tools 가 있으면 envelope, 스키마만 있으면 output format, 둘 다 없으면 undefined", () => {
144
+ expect(renderSchemaContractPrompt({ tools: TOOLS, jsonSchema: USER_SCHEMA })).toContain(
145
+ "Client Tool Protocol",
146
+ );
147
+ expect(renderSchemaContractPrompt({ jsonSchema: USER_SCHEMA })).toContain("Output Format");
148
+ expect(renderSchemaContractPrompt({})).toBeUndefined();
149
+ expect(renderSchemaContractPrompt({ tools: [] })).toBeUndefined();
150
+ });
151
+ });
152
+
153
+ // 예시 JSON 이 실제 parseEnvelope 를 통과하는 교차 계약 테스트는 tool-emulation.test.ts 에
154
+ // 있다("schema-prompt 예시 ↔ envelope 교차 계약") — 이 파일은 렌더러만 import 해 가볍게
155
+ // 유지한다(isolate:false 공유 레지스트리에서 실모듈 로드가 mock 파일을 오염시키는 것 방지).
156
+ describe("envelope 예시 문자열", () => {
157
+ it("렌더 문서에 실리는 예시 문자열이 실제로 그 문서 안에 있다", () => {
158
+ const { toolCallExample, textAnswerExample } = buildEnvelopeExamples(TOOLS);
159
+
160
+ expect(renderToolEnvelopePrompt(TOOLS)).toContain(toolCallExample!);
161
+ expect(renderToolEnvelopePrompt(TOOLS)).toContain(textAnswerExample);
162
+ // json answer 변형은 유효한 리터럴 예시를 만들 수 없으므로 예시 없이 프로즈로만 —
163
+ // text answer 예시(string)가 json 모드 문서에 섞이면 모델이 string 을 베낀다.
164
+ expect(renderToolEnvelopePrompt(TOOLS, USER_SCHEMA)).not.toContain(textAnswerExample);
165
+ });
166
+ });