@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,358 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ aggregateBench,
5
+ type BenchRecord,
6
+ checkpointKey,
7
+ classifyBenchText,
8
+ fingerprintFixture,
9
+ formatAggregateTable,
10
+ parseCheckpointLines,
11
+ validateAgainstSchema,
12
+ } from "./schema-bench";
13
+
14
+ describe("classifyBenchText — 3계층 배타 분류", () => {
15
+ const opts = {
16
+ requiredTopLevelKeys: ["title"],
17
+ allowedTopLevelKeys: ["title", "mood"],
18
+ };
19
+
20
+ it("정상 응답은 syntax/contract ok, 위반 없음", () => {
21
+ const c = classifyBenchText('{"title":"밤의 극장","mood":null}', opts);
22
+ expect(c).toMatchObject({
23
+ syntax: "ok",
24
+ contract: "ok",
25
+ topLevel: { wrapperKey: false, fenceResidue: false, prose: false },
26
+ });
27
+ });
28
+
29
+ it("JSON 문법 실패는 syntax 계층에만 잡히고 contract 는 n/a", () => {
30
+ const c = classifyBenchText('{"title": broken', opts);
31
+ expect(c.syntax).toBe("fail");
32
+ expect(c.contract).toBe("n/a");
33
+ });
34
+
35
+ it("필드 누락은 contract 계층에만 집계된다 (top-level 축 아님)", () => {
36
+ const c = classifyBenchText('{"mood":"tense"}', opts);
37
+ expect(c.syntax).toBe("ok");
38
+ expect(c.contract).toBe("fail");
39
+ expect(c.contractDetail).toContain("title");
40
+ expect(c.topLevel.wrapperKey).toBe(false);
41
+ });
42
+
43
+ it("래퍼 키 병리(GEN-359)를 top-level 축으로 분류한다", () => {
44
+ const c = classifyBenchText('{"$PARAMETER_NAME":{"title":"x"}}', opts);
45
+ expect(c.syntax).toBe("ok");
46
+ expect(c.contract).toBe("fail"); // required 누락이기도 하다
47
+ expect(c.topLevel.wrapperKey).toBe(true);
48
+ });
49
+
50
+ it("펜스 잔존과 프로즈를 구분한다", () => {
51
+ const fenced = classifyBenchText('```json\n{"title":"x"}\n```', opts);
52
+ expect(fenced.syntax).toBe("fail");
53
+ expect(fenced.topLevel.fenceResidue).toBe(true);
54
+ expect(fenced.topLevel.prose).toBe(false);
55
+
56
+ const prose = classifyBenchText("Sure! Here is the JSON you asked for.", opts);
57
+ expect(prose.topLevel.prose).toBe(true);
58
+ expect(prose.topLevel.fenceResidue).toBe(false);
59
+ });
60
+
61
+ it("fixture validate 실패는 contract fail 로 집계된다", () => {
62
+ const c = classifyBenchText('{"title":"x"}', {
63
+ ...opts,
64
+ validate: (parsed) =>
65
+ (parsed as { title: string }).title.length < 2 ? "title too short" : undefined,
66
+ });
67
+ expect(c.contract).toBe("fail");
68
+ expect(c.contractDetail).toBe("title too short");
69
+ });
70
+
71
+ it("허용 키 목록이 없으면 래퍼 키 판정을 하지 않는다", () => {
72
+ const c = classifyBenchText('{"anything":{"nested":1}}', {});
73
+ expect(c.topLevel.wrapperKey).toBe(false);
74
+ expect(c.contract).toBe("ok");
75
+ });
76
+ });
77
+
78
+ describe("validateAgainstSchema — 전체 트리 검증", () => {
79
+ const nested = {
80
+ type: "object",
81
+ properties: {
82
+ scenes: {
83
+ type: "array",
84
+ minItems: 3,
85
+ items: {
86
+ type: "object",
87
+ properties: {
88
+ id: { type: "string" },
89
+ advance: { type: "string", enum: ["next", "choice"] },
90
+ },
91
+ required: ["id", "advance"],
92
+ },
93
+ },
94
+ },
95
+ required: ["scenes"],
96
+ };
97
+
98
+ it("top-level 키만 있고 속이 빈 응답을 거부한다 (SON-495 교훈)", () => {
99
+ // 상위 키 판정만 하면 이게 통과한다 — 심층 검증의 존재 이유
100
+ const errs = validateAgainstSchema({ scenes: [{}, {}, {}] }, nested);
101
+ expect(errs.length).toBeGreaterThanOrEqual(6); // 각 scene 의 id·advance 누락
102
+ expect(errs[0]).toContain("must have required property");
103
+ });
104
+
105
+ it("타입·enum·minItems 위반을 경로와 함께 보고한다", () => {
106
+ expect(validateAgainstSchema({ scenes: [{ id: 1, advance: "next" }] }, nested)).toEqual(
107
+ expect.arrayContaining([expect.stringContaining("fewer than 3 items")]),
108
+ );
109
+ const deep = validateAgainstSchema(
110
+ {
111
+ scenes: [
112
+ { id: "s1", advance: "jump" },
113
+ { id: "s2", advance: "next" },
114
+ { id: "s3", advance: "next" },
115
+ ],
116
+ },
117
+ nested,
118
+ );
119
+ expect(deep).toEqual([
120
+ expect.stringContaining("/scenes/0/advance: must be equal to one of the allowed values"),
121
+ ]);
122
+ });
123
+
124
+ it("anyOf/const/nullable union 을 지원한다 (deti 실물 keyword)", () => {
125
+ const sch = {
126
+ type: "object",
127
+ properties: {
128
+ mood: { type: ["string", "null"] },
129
+ kind: { anyOf: [{ const: "a" }, { const: "b" }] },
130
+ },
131
+ required: ["mood", "kind"],
132
+ };
133
+ expect(validateAgainstSchema({ mood: null, kind: "a" }, sch)).toEqual([]);
134
+ // allErrors 라 브랜치별 세부 에러가 함께 나온다 — 두 필드 모두 위반이 잡히는 것만 고정
135
+ const errs = validateAgainstSchema({ mood: 3, kind: "c" }, sch);
136
+ expect(errs.some((e) => e.startsWith("/mood:"))).toBe(true);
137
+ expect(errs.some((e) => e.startsWith("/kind:"))).toBe(true);
138
+ });
139
+
140
+ it("oneOf 는 정확히 한 브랜치만 허용한다 — anyOf 로 완화하지 않는다 (2차 검토 #2)", () => {
141
+ const sch = {
142
+ oneOf: [{ type: "object", required: ["a"] }, { type: "object" }],
143
+ };
144
+ // 두 브랜치 모두 매칭 → oneOf 위반. 축약 검증기는 이걸 통과시켰다.
145
+ expect(validateAgainstSchema({ a: 1 }, sch)).toEqual(
146
+ expect.arrayContaining([expect.stringContaining("exactly one")]),
147
+ );
148
+ expect(validateAgainstSchema({ b: 1 }, sch)).toEqual([]);
149
+ });
150
+
151
+ it("실측 반증 케이스: anyOf 브랜치 내부의 enum 위반을 잡는다 (e2e 4/4→2/4 원인)", () => {
152
+ // v1 e2e 오판의 축소판 — expression 이 anyOf 브랜치 안 enum 에 걸림
153
+ const sch = {
154
+ type: "object",
155
+ properties: {
156
+ elements: {
157
+ type: "array",
158
+ items: {
159
+ anyOf: [
160
+ {
161
+ type: "object",
162
+ properties: { expression: { type: "string", enum: ["평온", "긴장"] } },
163
+ required: ["expression"],
164
+ },
165
+ { type: "null" },
166
+ ],
167
+ },
168
+ },
169
+ },
170
+ required: ["elements"],
171
+ };
172
+ expect(validateAgainstSchema({ elements: [{ expression: "기대" }] }, sch)).not.toEqual([]);
173
+ expect(validateAgainstSchema({ elements: [{ expression: "평온" }, null] }, sch)).toEqual([]);
174
+ });
175
+
176
+ it("정상 인스턴스는 위반 0", () => {
177
+ expect(
178
+ validateAgainstSchema(
179
+ { scenes: [{ id: "1", advance: "next" }, { id: "2", advance: "choice" }, { id: "3", advance: "next" }] },
180
+ nested,
181
+ ),
182
+ ).toEqual([]);
183
+ });
184
+ });
185
+
186
+ describe("classifyBenchText — plainText 계약", () => {
187
+ it("평문 answer 를 JSON 실패로 둔갑시키지 않는다 (tools-only 최종 답변)", () => {
188
+ const c = classifyBenchText("사건 파일 요약: 1976년 단체 사진 사건입니다.", {
189
+ plainText: true,
190
+ validate: (_p, raw) => (raw.trim().length > 0 ? undefined : "empty"),
191
+ });
192
+ expect(c).toMatchObject({
193
+ syntax: "ok",
194
+ contract: "ok",
195
+ topLevel: { prose: false, fenceResidue: false },
196
+ });
197
+ });
198
+
199
+ it("펜스 잔존은 평문 계약에서도 잡는다", () => {
200
+ const c = classifyBenchText("```\n요약\n```", { plainText: true });
201
+ expect(c.topLevel.fenceResidue).toBe(true);
202
+ });
203
+ });
204
+
205
+ describe("classifyBenchText — schema 옵션이 심층 검증을 켠다", () => {
206
+ it("속 빈 nested 응답이 contract fail 로 잡힌다", () => {
207
+ const c = classifyBenchText('{"scenes":[{},{},{}]}', {
208
+ schema: {
209
+ type: "object",
210
+ properties: {
211
+ scenes: { type: "array", items: { type: "object", required: ["id"], properties: { id: { type: "string" } } } },
212
+ },
213
+ required: ["scenes"],
214
+ },
215
+ });
216
+ expect(c.contract).toBe("fail");
217
+ expect(c.contractDetail).toContain("schema violations");
218
+ });
219
+ });
220
+
221
+ describe("fingerprintFixture", () => {
222
+ it("내용이 다르면 지문이 다르고, 같으면 같다 — checkpoint 동일성의 근거", () => {
223
+ const a = fingerprintFixture({ prompt: "p", jsonSchema: "{}" });
224
+ expect(fingerprintFixture({ prompt: "p", jsonSchema: "{}" })).toBe(a);
225
+ expect(fingerprintFixture({ prompt: "p2", jsonSchema: "{}" })).not.toBe(a);
226
+ expect(fingerprintFixture({ prompt: "p", jsonSchema: '{"a":1}' })).not.toBe(a);
227
+ expect(fingerprintFixture({ prompt: "p", jsonSchema: "{}", tools: [{ name: "t" }] })).not.toBe(a);
228
+ });
229
+
230
+ it("지문이 checkpoint 키에 반영돼 fixture 수정 후 옛 결과가 재사용되지 않는다", () => {
231
+ const base = { fixture: "f", model: "m", mode: "generate" as const, iteration: 0 };
232
+ expect(checkpointKey({ ...base, fixtureHash: "aaaa" })).not.toBe(
233
+ checkpointKey({ ...base, fixtureHash: "bbbb" }),
234
+ );
235
+ });
236
+ });
237
+
238
+ describe("checkpoint", () => {
239
+ const record: BenchRecord = {
240
+ fixture: "schema-small",
241
+ model: "anthropic/claude-sonnet-4-6",
242
+ mode: "generate",
243
+ iteration: 3,
244
+ durationMs: 1200,
245
+ };
246
+
247
+ it("동일 조합은 같은 키 — resume 시 재발사가 스킵된다", () => {
248
+ const withDifferentDuration: BenchRecord = { ...record, durationMs: 99 };
249
+ expect(checkpointKey(record)).toBe(checkpointKey(withDifferentDuration));
250
+ expect(checkpointKey(record)).not.toBe(checkpointKey({ ...record, iteration: 4 }));
251
+ expect(checkpointKey(record)).not.toBe(checkpointKey({ ...record, mode: "stream" }));
252
+ });
253
+
254
+ it("JSONL 을 파싱하고 부분 기록된 깨진 줄은 건너뛴다", () => {
255
+ const jsonl = [
256
+ JSON.stringify(record),
257
+ JSON.stringify({ ...record, iteration: 4 }),
258
+ '{"fixture":"schema-small","model":"an', // 중단으로 잘린 마지막 줄
259
+ ].join("\n");
260
+
261
+ const map = parseCheckpointLines(jsonl);
262
+ expect(map.size).toBe(2);
263
+ expect(map.has(checkpointKey(record))).toBe(true);
264
+ });
265
+ });
266
+
267
+ describe("aggregateBench", () => {
268
+ function rec(overrides: Partial<BenchRecord>): BenchRecord {
269
+ return {
270
+ fixture: "schema-small",
271
+ model: "m",
272
+ mode: "generate",
273
+ iteration: 0,
274
+ durationMs: 100,
275
+ classification: {
276
+ syntax: "ok",
277
+ contract: "ok",
278
+ topLevel: { wrapperKey: false, fenceResidue: false, prose: false },
279
+ },
280
+ ...overrides,
281
+ };
282
+ }
283
+
284
+ it("전송 오류를 분모에 포함해 통과율을 계산한다", () => {
285
+ const rows = aggregateBench([
286
+ rec({ iteration: 0 }),
287
+ rec({ iteration: 1 }),
288
+ rec({ iteration: 2, transportError: "socket hang up", classification: undefined }),
289
+ rec({
290
+ iteration: 3,
291
+ classification: {
292
+ syntax: "fail",
293
+ contract: "n/a",
294
+ topLevel: { wrapperKey: false, fenceResidue: true, prose: false },
295
+ },
296
+ }),
297
+ ]);
298
+
299
+ expect(rows).toHaveLength(1);
300
+ const row = rows[0]!;
301
+ expect(row.total).toBe(4);
302
+ expect(row.passes).toBe(2);
303
+ expect(row.passRate).toBe(0.5);
304
+ expect(row.transportErrors).toBe(1);
305
+ expect(row.syntaxFails).toBe(1);
306
+ expect(row.fenceResidue).toBe(1);
307
+ });
308
+
309
+ it("모델×fixture×mode 로 그룹화한다", () => {
310
+ const rows = aggregateBench([
311
+ rec({ model: "a" }),
312
+ rec({ model: "a", mode: "stream" }),
313
+ rec({ model: "b" }),
314
+ ]);
315
+ expect(rows).toHaveLength(3);
316
+ });
317
+
318
+ it("duration 평균과 토큰·비용 합계를 낸다", () => {
319
+ const rows = aggregateBench([
320
+ rec({ iteration: 0, durationMs: 100, outputTokens: 10, costUsd: 0.01 }),
321
+ rec({ iteration: 1, durationMs: 300, outputTokens: 20, costUsd: 0.02 }),
322
+ ]);
323
+ expect(rows[0]).toMatchObject({
324
+ avgDurationMs: 200,
325
+ totalOutputTokens: 30,
326
+ });
327
+ expect(rows[0]!.totalCostUsd).toBeCloseTo(0.03);
328
+ });
329
+
330
+ it("델타≠done 불일치를 집계하고 pass 에서도 제외한다 (2차 검토 #7)", () => {
331
+ const rows = aggregateBench([rec({ deltaDoneMismatch: true }), rec({ iteration: 1 })]);
332
+ expect(rows[0]!.deltaDoneMismatches).toBe(1);
333
+ expect(rows[0]!.passes).toBe(1); // mismatch 건은 syntax/contract ok 여도 pass 아님
334
+ });
335
+
336
+ it("형태 위반(펜스 잔존 등)은 contract ok 여도 pass 가 아니다 (2차 검토 #7)", () => {
337
+ const rows = aggregateBench([
338
+ rec({
339
+ classification: {
340
+ syntax: "ok",
341
+ contract: "ok",
342
+ topLevel: { wrapperKey: false, fenceResidue: true, prose: false },
343
+ },
344
+ }),
345
+ rec({ iteration: 1 }),
346
+ ]);
347
+ expect(rows[0]!.passes).toBe(1);
348
+ expect(rows[0]!.fenceResidue).toBe(1);
349
+ });
350
+
351
+ it("표 렌더가 헤더+행 형태를 유지한다", () => {
352
+ const table = formatAggregateTable(aggregateBench([rec({})]));
353
+ const lines = table.split("\n");
354
+ expect(lines[0]).toContain("rate");
355
+ expect(lines).toHaveLength(2);
356
+ expect(lines[1]).toContain("100.0%");
357
+ });
358
+ });