@maintainabilityai/research-runner 0.1.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.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bin/research-runner.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +209 -0
- package/dist/llm/anthropic-client.d.ts +39 -0
- package/dist/llm/anthropic-client.js +74 -0
- package/dist/llm/github-models-client.d.ts +46 -0
- package/dist/llm/github-models-client.js +78 -0
- package/dist/llm/llm-router.d.ts +46 -0
- package/dist/llm/llm-router.js +60 -0
- package/dist/mesh/get-mesh-sha.d.ts +1 -0
- package/dist/mesh/get-mesh-sha.js +27 -0
- package/dist/mesh/mesh-reader.d.ts +14 -0
- package/dist/mesh/mesh-reader.js +392 -0
- package/dist/mesh/prompt-loader.d.ts +22 -0
- package/dist/mesh/prompt-loader.js +119 -0
- package/dist/mesh/threat-model-reader.d.ts +33 -0
- package/dist/mesh/threat-model-reader.js +123 -0
- package/dist/runner/archeologist.d.ts +39 -0
- package/dist/runner/archeologist.js +620 -0
- package/dist/runner/audit-emitter.d.ts +62 -0
- package/dist/runner/audit-emitter.js +210 -0
- package/dist/runner/hatters-tag-builder.d.ts +52 -0
- package/dist/runner/hatters-tag-builder.js +40 -0
- package/dist/runner/nodes/analyze-architecture.d.ts +10 -0
- package/dist/runner/nodes/analyze-architecture.js +447 -0
- package/dist/runner/nodes/arxiv-search.d.ts +12 -0
- package/dist/runner/nodes/arxiv-search.js +52 -0
- package/dist/runner/nodes/clone-and-index.d.ts +32 -0
- package/dist/runner/nodes/clone-and-index.js +158 -0
- package/dist/runner/nodes/dedupe-and-rank.d.ts +27 -0
- package/dist/runner/nodes/dedupe-and-rank.js +98 -0
- package/dist/runner/nodes/deterministic-review.d.ts +55 -0
- package/dist/runner/nodes/deterministic-review.js +206 -0
- package/dist/runner/nodes/expert-review.d.ts +68 -0
- package/dist/runner/nodes/expert-review.js +197 -0
- package/dist/runner/nodes/gap-analysis.d.ts +48 -0
- package/dist/runner/nodes/gap-analysis.js +153 -0
- package/dist/runner/nodes/generate-prd-manifest.d.ts +53 -0
- package/dist/runner/nodes/generate-prd-manifest.js +209 -0
- package/dist/runner/nodes/hackernews-search.d.ts +12 -0
- package/dist/runner/nodes/hackernews-search.js +63 -0
- package/dist/runner/nodes/identify-gaps.d.ts +33 -0
- package/dist/runner/nodes/identify-gaps.js +185 -0
- package/dist/runner/nodes/plan-queries.d.ts +28 -0
- package/dist/runner/nodes/plan-queries.js +120 -0
- package/dist/runner/nodes/prd-validator.d.ts +51 -0
- package/dist/runner/nodes/prd-validator.js +203 -0
- package/dist/runner/nodes/synthesis-archaeology-validator.d.ts +22 -0
- package/dist/runner/nodes/synthesis-archaeology-validator.js +131 -0
- package/dist/runner/nodes/synthesis-validator.d.ts +51 -0
- package/dist/runner/nodes/synthesis-validator.js +185 -0
- package/dist/runner/nodes/synthesize-prd.d.ts +84 -0
- package/dist/runner/nodes/synthesize-prd.js +202 -0
- package/dist/runner/nodes/synthesize-report.d.ts +53 -0
- package/dist/runner/nodes/synthesize-report.js +188 -0
- package/dist/runner/nodes/tavily-search.d.ts +21 -0
- package/dist/runner/nodes/tavily-search.js +57 -0
- package/dist/runner/nodes/uspto-search.d.ts +13 -0
- package/dist/runner/nodes/uspto-search.js +62 -0
- package/dist/runner/nodes/verify-grounding.d.ts +54 -0
- package/dist/runner/nodes/verify-grounding.js +134 -0
- package/dist/runner/prd.d.ts +28 -0
- package/dist/runner/prd.js +494 -0
- package/dist/schemas/audit-event.d.ts +1151 -0
- package/dist/schemas/audit-event.js +141 -0
- package/dist/schemas/index.d.ts +17 -0
- package/dist/schemas/index.js +33 -0
- package/dist/schemas/mesh-context.d.ts +415 -0
- package/dist/schemas/mesh-context.js +95 -0
- package/dist/schemas/observed-architecture.d.ts +262 -0
- package/dist/schemas/observed-architecture.js +90 -0
- package/dist/schemas/prd-brief.d.ts +111 -0
- package/dist/schemas/prd-brief.js +37 -0
- package/dist/schemas/prd-doc.d.ts +249 -0
- package/dist/schemas/prd-doc.js +42 -0
- package/dist/schemas/prd-manifest.d.ts +171 -0
- package/dist/schemas/prd-manifest.js +73 -0
- package/dist/schemas/primitives.d.ts +47 -0
- package/dist/schemas/primitives.js +41 -0
- package/dist/schemas/query-plan.d.ts +33 -0
- package/dist/schemas/query-plan.js +25 -0
- package/dist/schemas/ranked-source.d.ts +82 -0
- package/dist/schemas/ranked-source.js +29 -0
- package/dist/schemas/research-brief.d.ts +114 -0
- package/dist/schemas/research-brief.js +49 -0
- package/dist/schemas/research-doc.d.ts +104 -0
- package/dist/schemas/research-doc.js +37 -0
- package/dist/search/arxiv-client.d.ts +41 -0
- package/dist/search/arxiv-client.js +88 -0
- package/dist/search/hackernews-client.d.ts +33 -0
- package/dist/search/hackernews-client.js +44 -0
- package/dist/search/provider-result.d.ts +25 -0
- package/dist/search/provider-result.js +2 -0
- package/dist/search/tavily-client.d.ts +38 -0
- package/dist/search/tavily-client.js +53 -0
- package/dist/search/uspto-client.d.ts +50 -0
- package/dist/search/uspto-client.js +112 -0
- package/dist/utils/run-id.d.ts +2 -0
- package/dist/utils/run-id.js +22 -0
- package/package.json +53 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrdDoc — the published PRD artifact + the grounding block the refinement
|
|
3
|
+
* loop accumulates over iterations.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export declare const GroundingBlock: z.ZodObject<{
|
|
7
|
+
final_iteration: z.ZodNumber;
|
|
8
|
+
/** Per-iteration scores from both experts (length = iterations * 2). */
|
|
9
|
+
iterations: z.ZodArray<z.ZodObject<{
|
|
10
|
+
expert: z.ZodEnum<["architecture", "security"]>;
|
|
11
|
+
iteration: z.ZodNumber;
|
|
12
|
+
score: z.ZodNumber;
|
|
13
|
+
severity: z.ZodEnum<["PASS", "MINOR", "MAJOR", "BLOCKING"]>;
|
|
14
|
+
covered_ids: z.ZodArray<z.ZodString, "many">;
|
|
15
|
+
missing_ids: z.ZodArray<z.ZodString, "many">;
|
|
16
|
+
changes: z.ZodArray<z.ZodString, "many">;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
expert: "architecture" | "security";
|
|
19
|
+
iteration: number;
|
|
20
|
+
score: number;
|
|
21
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
22
|
+
covered_ids: string[];
|
|
23
|
+
missing_ids: string[];
|
|
24
|
+
changes: string[];
|
|
25
|
+
}, {
|
|
26
|
+
expert: "architecture" | "security";
|
|
27
|
+
iteration: number;
|
|
28
|
+
score: number;
|
|
29
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
30
|
+
covered_ids: string[];
|
|
31
|
+
missing_ids: string[];
|
|
32
|
+
changes: string[];
|
|
33
|
+
}>, "many">;
|
|
34
|
+
/** Deterministic citation-coverage check independent of LLM scores. */
|
|
35
|
+
citation_coverage: z.ZodObject<{
|
|
36
|
+
threats_in_scope: z.ZodNumber;
|
|
37
|
+
threats_covered_by_sr: z.ZodNumber;
|
|
38
|
+
calm_nodes_in_scope: z.ZodNumber;
|
|
39
|
+
calm_nodes_cited_by_fr: z.ZodNumber;
|
|
40
|
+
self_reported_no_count: z.ZodNumber;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
threats_in_scope: number;
|
|
43
|
+
threats_covered_by_sr: number;
|
|
44
|
+
calm_nodes_in_scope: number;
|
|
45
|
+
calm_nodes_cited_by_fr: number;
|
|
46
|
+
self_reported_no_count: number;
|
|
47
|
+
}, {
|
|
48
|
+
threats_in_scope: number;
|
|
49
|
+
threats_covered_by_sr: number;
|
|
50
|
+
calm_nodes_in_scope: number;
|
|
51
|
+
calm_nodes_cited_by_fr: number;
|
|
52
|
+
self_reported_no_count: number;
|
|
53
|
+
}>;
|
|
54
|
+
/** Final composite score combining LLM expert + citation signals. */
|
|
55
|
+
final_score: z.ZodNumber;
|
|
56
|
+
passed: z.ZodBoolean;
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
final_iteration: number;
|
|
59
|
+
iterations: {
|
|
60
|
+
expert: "architecture" | "security";
|
|
61
|
+
iteration: number;
|
|
62
|
+
score: number;
|
|
63
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
64
|
+
covered_ids: string[];
|
|
65
|
+
missing_ids: string[];
|
|
66
|
+
changes: string[];
|
|
67
|
+
}[];
|
|
68
|
+
citation_coverage: {
|
|
69
|
+
threats_in_scope: number;
|
|
70
|
+
threats_covered_by_sr: number;
|
|
71
|
+
calm_nodes_in_scope: number;
|
|
72
|
+
calm_nodes_cited_by_fr: number;
|
|
73
|
+
self_reported_no_count: number;
|
|
74
|
+
};
|
|
75
|
+
final_score: number;
|
|
76
|
+
passed: boolean;
|
|
77
|
+
}, {
|
|
78
|
+
final_iteration: number;
|
|
79
|
+
iterations: {
|
|
80
|
+
expert: "architecture" | "security";
|
|
81
|
+
iteration: number;
|
|
82
|
+
score: number;
|
|
83
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
84
|
+
covered_ids: string[];
|
|
85
|
+
missing_ids: string[];
|
|
86
|
+
changes: string[];
|
|
87
|
+
}[];
|
|
88
|
+
citation_coverage: {
|
|
89
|
+
threats_in_scope: number;
|
|
90
|
+
threats_covered_by_sr: number;
|
|
91
|
+
calm_nodes_in_scope: number;
|
|
92
|
+
calm_nodes_cited_by_fr: number;
|
|
93
|
+
self_reported_no_count: number;
|
|
94
|
+
};
|
|
95
|
+
final_score: number;
|
|
96
|
+
passed: boolean;
|
|
97
|
+
}>;
|
|
98
|
+
export type GroundingBlock = z.infer<typeof GroundingBlock>;
|
|
99
|
+
export declare const PrdDoc: z.ZodObject<{
|
|
100
|
+
run_id: z.ZodString;
|
|
101
|
+
topic: z.ZodString;
|
|
102
|
+
generated_at: z.ZodEffects<z.ZodString, string, string>;
|
|
103
|
+
body_md: z.ZodString;
|
|
104
|
+
grounding: z.ZodObject<{
|
|
105
|
+
final_iteration: z.ZodNumber;
|
|
106
|
+
/** Per-iteration scores from both experts (length = iterations * 2). */
|
|
107
|
+
iterations: z.ZodArray<z.ZodObject<{
|
|
108
|
+
expert: z.ZodEnum<["architecture", "security"]>;
|
|
109
|
+
iteration: z.ZodNumber;
|
|
110
|
+
score: z.ZodNumber;
|
|
111
|
+
severity: z.ZodEnum<["PASS", "MINOR", "MAJOR", "BLOCKING"]>;
|
|
112
|
+
covered_ids: z.ZodArray<z.ZodString, "many">;
|
|
113
|
+
missing_ids: z.ZodArray<z.ZodString, "many">;
|
|
114
|
+
changes: z.ZodArray<z.ZodString, "many">;
|
|
115
|
+
}, "strip", z.ZodTypeAny, {
|
|
116
|
+
expert: "architecture" | "security";
|
|
117
|
+
iteration: number;
|
|
118
|
+
score: number;
|
|
119
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
120
|
+
covered_ids: string[];
|
|
121
|
+
missing_ids: string[];
|
|
122
|
+
changes: string[];
|
|
123
|
+
}, {
|
|
124
|
+
expert: "architecture" | "security";
|
|
125
|
+
iteration: number;
|
|
126
|
+
score: number;
|
|
127
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
128
|
+
covered_ids: string[];
|
|
129
|
+
missing_ids: string[];
|
|
130
|
+
changes: string[];
|
|
131
|
+
}>, "many">;
|
|
132
|
+
/** Deterministic citation-coverage check independent of LLM scores. */
|
|
133
|
+
citation_coverage: z.ZodObject<{
|
|
134
|
+
threats_in_scope: z.ZodNumber;
|
|
135
|
+
threats_covered_by_sr: z.ZodNumber;
|
|
136
|
+
calm_nodes_in_scope: z.ZodNumber;
|
|
137
|
+
calm_nodes_cited_by_fr: z.ZodNumber;
|
|
138
|
+
self_reported_no_count: z.ZodNumber;
|
|
139
|
+
}, "strip", z.ZodTypeAny, {
|
|
140
|
+
threats_in_scope: number;
|
|
141
|
+
threats_covered_by_sr: number;
|
|
142
|
+
calm_nodes_in_scope: number;
|
|
143
|
+
calm_nodes_cited_by_fr: number;
|
|
144
|
+
self_reported_no_count: number;
|
|
145
|
+
}, {
|
|
146
|
+
threats_in_scope: number;
|
|
147
|
+
threats_covered_by_sr: number;
|
|
148
|
+
calm_nodes_in_scope: number;
|
|
149
|
+
calm_nodes_cited_by_fr: number;
|
|
150
|
+
self_reported_no_count: number;
|
|
151
|
+
}>;
|
|
152
|
+
/** Final composite score combining LLM expert + citation signals. */
|
|
153
|
+
final_score: z.ZodNumber;
|
|
154
|
+
passed: z.ZodBoolean;
|
|
155
|
+
}, "strip", z.ZodTypeAny, {
|
|
156
|
+
final_iteration: number;
|
|
157
|
+
iterations: {
|
|
158
|
+
expert: "architecture" | "security";
|
|
159
|
+
iteration: number;
|
|
160
|
+
score: number;
|
|
161
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
162
|
+
covered_ids: string[];
|
|
163
|
+
missing_ids: string[];
|
|
164
|
+
changes: string[];
|
|
165
|
+
}[];
|
|
166
|
+
citation_coverage: {
|
|
167
|
+
threats_in_scope: number;
|
|
168
|
+
threats_covered_by_sr: number;
|
|
169
|
+
calm_nodes_in_scope: number;
|
|
170
|
+
calm_nodes_cited_by_fr: number;
|
|
171
|
+
self_reported_no_count: number;
|
|
172
|
+
};
|
|
173
|
+
final_score: number;
|
|
174
|
+
passed: boolean;
|
|
175
|
+
}, {
|
|
176
|
+
final_iteration: number;
|
|
177
|
+
iterations: {
|
|
178
|
+
expert: "architecture" | "security";
|
|
179
|
+
iteration: number;
|
|
180
|
+
score: number;
|
|
181
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
182
|
+
covered_ids: string[];
|
|
183
|
+
missing_ids: string[];
|
|
184
|
+
changes: string[];
|
|
185
|
+
}[];
|
|
186
|
+
citation_coverage: {
|
|
187
|
+
threats_in_scope: number;
|
|
188
|
+
threats_covered_by_sr: number;
|
|
189
|
+
calm_nodes_in_scope: number;
|
|
190
|
+
calm_nodes_cited_by_fr: number;
|
|
191
|
+
self_reported_no_count: number;
|
|
192
|
+
};
|
|
193
|
+
final_score: number;
|
|
194
|
+
passed: boolean;
|
|
195
|
+
}>;
|
|
196
|
+
}, "strip", z.ZodTypeAny, {
|
|
197
|
+
topic: string;
|
|
198
|
+
run_id: string;
|
|
199
|
+
generated_at: string;
|
|
200
|
+
body_md: string;
|
|
201
|
+
grounding: {
|
|
202
|
+
final_iteration: number;
|
|
203
|
+
iterations: {
|
|
204
|
+
expert: "architecture" | "security";
|
|
205
|
+
iteration: number;
|
|
206
|
+
score: number;
|
|
207
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
208
|
+
covered_ids: string[];
|
|
209
|
+
missing_ids: string[];
|
|
210
|
+
changes: string[];
|
|
211
|
+
}[];
|
|
212
|
+
citation_coverage: {
|
|
213
|
+
threats_in_scope: number;
|
|
214
|
+
threats_covered_by_sr: number;
|
|
215
|
+
calm_nodes_in_scope: number;
|
|
216
|
+
calm_nodes_cited_by_fr: number;
|
|
217
|
+
self_reported_no_count: number;
|
|
218
|
+
};
|
|
219
|
+
final_score: number;
|
|
220
|
+
passed: boolean;
|
|
221
|
+
};
|
|
222
|
+
}, {
|
|
223
|
+
topic: string;
|
|
224
|
+
run_id: string;
|
|
225
|
+
generated_at: string;
|
|
226
|
+
body_md: string;
|
|
227
|
+
grounding: {
|
|
228
|
+
final_iteration: number;
|
|
229
|
+
iterations: {
|
|
230
|
+
expert: "architecture" | "security";
|
|
231
|
+
iteration: number;
|
|
232
|
+
score: number;
|
|
233
|
+
severity: "PASS" | "MINOR" | "MAJOR" | "BLOCKING";
|
|
234
|
+
covered_ids: string[];
|
|
235
|
+
missing_ids: string[];
|
|
236
|
+
changes: string[];
|
|
237
|
+
}[];
|
|
238
|
+
citation_coverage: {
|
|
239
|
+
threats_in_scope: number;
|
|
240
|
+
threats_covered_by_sr: number;
|
|
241
|
+
calm_nodes_in_scope: number;
|
|
242
|
+
calm_nodes_cited_by_fr: number;
|
|
243
|
+
self_reported_no_count: number;
|
|
244
|
+
};
|
|
245
|
+
final_score: number;
|
|
246
|
+
passed: boolean;
|
|
247
|
+
};
|
|
248
|
+
}>;
|
|
249
|
+
export type PrdDoc = z.infer<typeof PrdDoc>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PrdDoc = exports.GroundingBlock = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* PrdDoc — the published PRD artifact + the grounding block the refinement
|
|
6
|
+
* loop accumulates over iterations.
|
|
7
|
+
*/
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
const primitives_1 = require("./primitives");
|
|
10
|
+
/** One iteration's expert review record — both arch + sec contribute one each. */
|
|
11
|
+
const ExpertReviewRecord = zod_1.z.object({
|
|
12
|
+
expert: zod_1.z.enum(['architecture', 'security']),
|
|
13
|
+
iteration: zod_1.z.number().int().min(1).max(5),
|
|
14
|
+
score: zod_1.z.number().min(0).max(1),
|
|
15
|
+
severity: zod_1.z.enum(['PASS', 'MINOR', 'MAJOR', 'BLOCKING']),
|
|
16
|
+
covered_ids: zod_1.z.array(zod_1.z.string()),
|
|
17
|
+
missing_ids: zod_1.z.array(zod_1.z.string()),
|
|
18
|
+
changes: zod_1.z.array(zod_1.z.string()),
|
|
19
|
+
});
|
|
20
|
+
exports.GroundingBlock = zod_1.z.object({
|
|
21
|
+
final_iteration: zod_1.z.number().int().min(1).max(5),
|
|
22
|
+
/** Per-iteration scores from both experts (length = iterations * 2). */
|
|
23
|
+
iterations: zod_1.z.array(ExpertReviewRecord),
|
|
24
|
+
/** Deterministic citation-coverage check independent of LLM scores. */
|
|
25
|
+
citation_coverage: zod_1.z.object({
|
|
26
|
+
threats_in_scope: zod_1.z.number().int().nonnegative(),
|
|
27
|
+
threats_covered_by_sr: zod_1.z.number().int().nonnegative(),
|
|
28
|
+
calm_nodes_in_scope: zod_1.z.number().int().nonnegative(),
|
|
29
|
+
calm_nodes_cited_by_fr: zod_1.z.number().int().nonnegative(),
|
|
30
|
+
self_reported_no_count: zod_1.z.number().int().nonnegative(),
|
|
31
|
+
}),
|
|
32
|
+
/** Final composite score combining LLM expert + citation signals. */
|
|
33
|
+
final_score: zod_1.z.number().min(0).max(1),
|
|
34
|
+
passed: zod_1.z.boolean(),
|
|
35
|
+
});
|
|
36
|
+
exports.PrdDoc = zod_1.z.object({
|
|
37
|
+
run_id: primitives_1.RunId,
|
|
38
|
+
topic: zod_1.z.string(),
|
|
39
|
+
generated_at: primitives_1.IsoTimestamp,
|
|
40
|
+
body_md: zod_1.z.string().min(1),
|
|
41
|
+
grounding: exports.GroundingBlock,
|
|
42
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrdManifest — the machine-readable spec the spec-ready-handler reads on
|
|
3
|
+
* the code-repo side to generate an RCTRO implementation issue.
|
|
4
|
+
*
|
|
5
|
+
* Lives alongside the PRD markdown as `<topic>.manifest.json`.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
/**
|
|
9
|
+
* Per-BAR impact classification. The PRD manifest carries the same set of
|
|
10
|
+
* BARs the platform spans (when scope is platform) or just the single BAR
|
|
11
|
+
* (when scope is bar), each tagged with the confidence that PRD work
|
|
12
|
+
* actually touches it. `notify-code-repos.yml` reads this to decide which
|
|
13
|
+
* code repos get a landing-issue (HIGH only) and which get a footer
|
|
14
|
+
* mention as "other BARs in the platform — review needed?" (LOW).
|
|
15
|
+
*
|
|
16
|
+
* HIGH: the BAR owns at least one CALM node referenced by an endpoint, or
|
|
17
|
+
* owns at least one threat cited by a security requirement.
|
|
18
|
+
* LOW : the BAR is part of the platform but has no direct citation in
|
|
19
|
+
* the manifest. Could still be impacted (shared infra, dependency
|
|
20
|
+
* chains) but no concrete signal from the PRD.
|
|
21
|
+
*/
|
|
22
|
+
declare const ImpactedBar: z.ZodObject<{
|
|
23
|
+
bar_id: z.ZodString;
|
|
24
|
+
repos: z.ZodArray<z.ZodString, "many">;
|
|
25
|
+
confidence: z.ZodEnum<["high", "low"]>;
|
|
26
|
+
reasoning: z.ZodString;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
bar_id: string;
|
|
29
|
+
confidence: "high" | "low";
|
|
30
|
+
repos: string[];
|
|
31
|
+
reasoning: string;
|
|
32
|
+
}, {
|
|
33
|
+
bar_id: string;
|
|
34
|
+
confidence: "high" | "low";
|
|
35
|
+
repos: string[];
|
|
36
|
+
reasoning: string;
|
|
37
|
+
}>;
|
|
38
|
+
export type ImpactedBar = z.infer<typeof ImpactedBar>;
|
|
39
|
+
export declare const PrdManifest: z.ZodObject<{
|
|
40
|
+
run_id: z.ZodString;
|
|
41
|
+
prd_topic: z.ZodString;
|
|
42
|
+
/** Mesh repo SHA at PRD publish time. */
|
|
43
|
+
mesh_sha: z.ZodString;
|
|
44
|
+
/**
|
|
45
|
+
* Code repos `notify-code-repos.yml` opens a landing-issue in. Equals
|
|
46
|
+
* the union of `impacted_bars[].repos` for HIGH-confidence entries.
|
|
47
|
+
* LOW-confidence BARs are NOT included here — they only surface as
|
|
48
|
+
* footer mentions in the HIGH BARs' landing-issues.
|
|
49
|
+
*/
|
|
50
|
+
target_repos: z.ZodArray<z.ZodString, "many">;
|
|
51
|
+
/**
|
|
52
|
+
* Per-BAR impact classification. Drives which repos in `target_repos`
|
|
53
|
+
* get an issue and what "Why this repo?" reasoning the issue body
|
|
54
|
+
* carries. See ImpactedBar above for the HIGH vs LOW semantics.
|
|
55
|
+
*/
|
|
56
|
+
impacted_bars: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
57
|
+
bar_id: z.ZodString;
|
|
58
|
+
repos: z.ZodArray<z.ZodString, "many">;
|
|
59
|
+
confidence: z.ZodEnum<["high", "low"]>;
|
|
60
|
+
reasoning: z.ZodString;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
bar_id: string;
|
|
63
|
+
confidence: "high" | "low";
|
|
64
|
+
repos: string[];
|
|
65
|
+
reasoning: string;
|
|
66
|
+
}, {
|
|
67
|
+
bar_id: string;
|
|
68
|
+
confidence: "high" | "low";
|
|
69
|
+
repos: string[];
|
|
70
|
+
reasoning: string;
|
|
71
|
+
}>, "many">>;
|
|
72
|
+
endpoints: z.ZodArray<z.ZodObject<{
|
|
73
|
+
/** HTTP method + path, e.g. "POST /favorites". */
|
|
74
|
+
signature: z.ZodString;
|
|
75
|
+
/** CALM node id this endpoint resides in. */
|
|
76
|
+
calm_node: z.ZodString;
|
|
77
|
+
/** FR-NN id from the PRD body that specifies this endpoint. */
|
|
78
|
+
fr_id: z.ZodString;
|
|
79
|
+
}, "strip", z.ZodTypeAny, {
|
|
80
|
+
signature: string;
|
|
81
|
+
calm_node: string;
|
|
82
|
+
fr_id: string;
|
|
83
|
+
}, {
|
|
84
|
+
signature: string;
|
|
85
|
+
calm_node: string;
|
|
86
|
+
fr_id: string;
|
|
87
|
+
}>, "many">;
|
|
88
|
+
security_requirements: z.ZodArray<z.ZodObject<{
|
|
89
|
+
/** SR-NN id from the PRD body. */
|
|
90
|
+
id: z.ZodString;
|
|
91
|
+
/** Citations: THR-NNN, A0X, NIST-XX-NN — at least one required. */
|
|
92
|
+
citations: z.ZodArray<z.ZodString, "many">;
|
|
93
|
+
}, "strip", z.ZodTypeAny, {
|
|
94
|
+
id: string;
|
|
95
|
+
citations: string[];
|
|
96
|
+
}, {
|
|
97
|
+
id: string;
|
|
98
|
+
citations: string[];
|
|
99
|
+
}>, "many">;
|
|
100
|
+
/** Final grounding state from the refinement loop. */
|
|
101
|
+
grounding: z.ZodObject<{
|
|
102
|
+
final_score: z.ZodNumber;
|
|
103
|
+
threshold: z.ZodNumber;
|
|
104
|
+
iterations: z.ZodNumber;
|
|
105
|
+
passed: z.ZodBoolean;
|
|
106
|
+
}, "strip", z.ZodTypeAny, {
|
|
107
|
+
iterations: number;
|
|
108
|
+
final_score: number;
|
|
109
|
+
passed: boolean;
|
|
110
|
+
threshold: number;
|
|
111
|
+
}, {
|
|
112
|
+
iterations: number;
|
|
113
|
+
final_score: number;
|
|
114
|
+
passed: boolean;
|
|
115
|
+
threshold: number;
|
|
116
|
+
}>;
|
|
117
|
+
}, "strip", z.ZodTypeAny, {
|
|
118
|
+
mesh_sha: string;
|
|
119
|
+
run_id: string;
|
|
120
|
+
grounding: {
|
|
121
|
+
iterations: number;
|
|
122
|
+
final_score: number;
|
|
123
|
+
passed: boolean;
|
|
124
|
+
threshold: number;
|
|
125
|
+
};
|
|
126
|
+
prd_topic: string;
|
|
127
|
+
target_repos: string[];
|
|
128
|
+
impacted_bars: {
|
|
129
|
+
bar_id: string;
|
|
130
|
+
confidence: "high" | "low";
|
|
131
|
+
repos: string[];
|
|
132
|
+
reasoning: string;
|
|
133
|
+
}[];
|
|
134
|
+
endpoints: {
|
|
135
|
+
signature: string;
|
|
136
|
+
calm_node: string;
|
|
137
|
+
fr_id: string;
|
|
138
|
+
}[];
|
|
139
|
+
security_requirements: {
|
|
140
|
+
id: string;
|
|
141
|
+
citations: string[];
|
|
142
|
+
}[];
|
|
143
|
+
}, {
|
|
144
|
+
mesh_sha: string;
|
|
145
|
+
run_id: string;
|
|
146
|
+
grounding: {
|
|
147
|
+
iterations: number;
|
|
148
|
+
final_score: number;
|
|
149
|
+
passed: boolean;
|
|
150
|
+
threshold: number;
|
|
151
|
+
};
|
|
152
|
+
prd_topic: string;
|
|
153
|
+
target_repos: string[];
|
|
154
|
+
endpoints: {
|
|
155
|
+
signature: string;
|
|
156
|
+
calm_node: string;
|
|
157
|
+
fr_id: string;
|
|
158
|
+
}[];
|
|
159
|
+
security_requirements: {
|
|
160
|
+
id: string;
|
|
161
|
+
citations: string[];
|
|
162
|
+
}[];
|
|
163
|
+
impacted_bars?: {
|
|
164
|
+
bar_id: string;
|
|
165
|
+
confidence: "high" | "low";
|
|
166
|
+
repos: string[];
|
|
167
|
+
reasoning: string;
|
|
168
|
+
}[] | undefined;
|
|
169
|
+
}>;
|
|
170
|
+
export type PrdManifest = z.infer<typeof PrdManifest>;
|
|
171
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PrdManifest = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* PrdManifest — the machine-readable spec the spec-ready-handler reads on
|
|
6
|
+
* the code-repo side to generate an RCTRO implementation issue.
|
|
7
|
+
*
|
|
8
|
+
* Lives alongside the PRD markdown as `<topic>.manifest.json`.
|
|
9
|
+
*/
|
|
10
|
+
const zod_1 = require("zod");
|
|
11
|
+
const primitives_1 = require("./primitives");
|
|
12
|
+
const Endpoint = zod_1.z.object({
|
|
13
|
+
/** HTTP method + path, e.g. "POST /favorites". */
|
|
14
|
+
signature: zod_1.z.string(),
|
|
15
|
+
/** CALM node id this endpoint resides in. */
|
|
16
|
+
calm_node: zod_1.z.string(),
|
|
17
|
+
/** FR-NN id from the PRD body that specifies this endpoint. */
|
|
18
|
+
fr_id: zod_1.z.string().regex(/^FR-\d+$/),
|
|
19
|
+
});
|
|
20
|
+
const SecurityRequirement = zod_1.z.object({
|
|
21
|
+
/** SR-NN id from the PRD body. */
|
|
22
|
+
id: zod_1.z.string().regex(/^SR-\d+$/),
|
|
23
|
+
/** Citations: THR-NNN, A0X, NIST-XX-NN — at least one required. */
|
|
24
|
+
citations: zod_1.z.array(zod_1.z.string().regex(/^(THR-\d+|A\d{2}|NIST-[A-Z]{2}-\d+)$/)).min(1),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Per-BAR impact classification. The PRD manifest carries the same set of
|
|
28
|
+
* BARs the platform spans (when scope is platform) or just the single BAR
|
|
29
|
+
* (when scope is bar), each tagged with the confidence that PRD work
|
|
30
|
+
* actually touches it. `notify-code-repos.yml` reads this to decide which
|
|
31
|
+
* code repos get a landing-issue (HIGH only) and which get a footer
|
|
32
|
+
* mention as "other BARs in the platform — review needed?" (LOW).
|
|
33
|
+
*
|
|
34
|
+
* HIGH: the BAR owns at least one CALM node referenced by an endpoint, or
|
|
35
|
+
* owns at least one threat cited by a security requirement.
|
|
36
|
+
* LOW : the BAR is part of the platform but has no direct citation in
|
|
37
|
+
* the manifest. Could still be impacted (shared infra, dependency
|
|
38
|
+
* chains) but no concrete signal from the PRD.
|
|
39
|
+
*/
|
|
40
|
+
const ImpactedBar = zod_1.z.object({
|
|
41
|
+
bar_id: zod_1.z.string(),
|
|
42
|
+
repos: zod_1.z.array(zod_1.z.string().regex(/^[\w.-]+\/[\w.-]+$/)),
|
|
43
|
+
confidence: zod_1.z.enum(['high', 'low']),
|
|
44
|
+
reasoning: zod_1.z.string(),
|
|
45
|
+
});
|
|
46
|
+
exports.PrdManifest = zod_1.z.object({
|
|
47
|
+
run_id: primitives_1.RunId,
|
|
48
|
+
prd_topic: zod_1.z.string(),
|
|
49
|
+
/** Mesh repo SHA at PRD publish time. */
|
|
50
|
+
mesh_sha: primitives_1.GitSha,
|
|
51
|
+
/**
|
|
52
|
+
* Code repos `notify-code-repos.yml` opens a landing-issue in. Equals
|
|
53
|
+
* the union of `impacted_bars[].repos` for HIGH-confidence entries.
|
|
54
|
+
* LOW-confidence BARs are NOT included here — they only surface as
|
|
55
|
+
* footer mentions in the HIGH BARs' landing-issues.
|
|
56
|
+
*/
|
|
57
|
+
target_repos: zod_1.z.array(zod_1.z.string().regex(/^[\w.-]+\/[\w.-]+$/)).min(1),
|
|
58
|
+
/**
|
|
59
|
+
* Per-BAR impact classification. Drives which repos in `target_repos`
|
|
60
|
+
* get an issue and what "Why this repo?" reasoning the issue body
|
|
61
|
+
* carries. See ImpactedBar above for the HIGH vs LOW semantics.
|
|
62
|
+
*/
|
|
63
|
+
impacted_bars: zod_1.z.array(ImpactedBar).default([]),
|
|
64
|
+
endpoints: zod_1.z.array(Endpoint),
|
|
65
|
+
security_requirements: zod_1.z.array(SecurityRequirement),
|
|
66
|
+
/** Final grounding state from the refinement loop. */
|
|
67
|
+
grounding: zod_1.z.object({
|
|
68
|
+
final_score: zod_1.z.number().min(0).max(1),
|
|
69
|
+
threshold: zod_1.z.number().min(0).max(1),
|
|
70
|
+
iterations: zod_1.z.number().int().min(1).max(5),
|
|
71
|
+
passed: zod_1.z.boolean(),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared primitive enums + branded types used across every other schema.
|
|
3
|
+
* Importing from here keeps the surface coherent and lets downstream tests
|
|
4
|
+
* assert against canonical literal unions instead of free strings.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export declare const GuardrailMode: z.ZodEnum<["strict", "default", "lenient"]>;
|
|
8
|
+
export type GuardrailMode = z.infer<typeof GuardrailMode>;
|
|
9
|
+
export declare const GroundingMode: z.ZodEnum<["strict", "default", "lenient"]>;
|
|
10
|
+
export type GroundingMode = z.infer<typeof GroundingMode>;
|
|
11
|
+
export declare const LlmProvider: z.ZodEnum<["anthropic", "openai", "azure-openai", "github-models"]>;
|
|
12
|
+
export type LlmProvider = z.infer<typeof LlmProvider>;
|
|
13
|
+
/**
|
|
14
|
+
* Providers that route through GitHub Models / Copilot use the workflow's
|
|
15
|
+
* GITHUB_TOKEN with `permissions: models: read` — no separate API key needs
|
|
16
|
+
* to live as a repo secret.
|
|
17
|
+
*/
|
|
18
|
+
export declare function providerNeedsApiKey(p: LlmProvider): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Research / PRD scope. Portfolio scope was dropped — too abstract for
|
|
21
|
+
* the agents to produce a targeted PRD. A run must be anchored to a
|
|
22
|
+
* concrete architectural surface: a platform (multiple BARs sharing a
|
|
23
|
+
* CALM model) or a single BAR.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ScopeLevel: z.ZodEnum<["platform", "bar"]>;
|
|
26
|
+
export type ScopeLevel = z.infer<typeof ScopeLevel>;
|
|
27
|
+
export declare const ResearchPath: z.ZodEnum<["research", "archaeology"]>;
|
|
28
|
+
export type ResearchPath = z.infer<typeof ResearchPath>;
|
|
29
|
+
export declare const PrdMode: z.ZodEnum<["shallow", "deep"]>;
|
|
30
|
+
export type PrdMode = z.infer<typeof PrdMode>;
|
|
31
|
+
export declare const SearchProvider: z.ZodEnum<["tavily", "arxiv", "uspto", "hackernews"]>;
|
|
32
|
+
export type SearchProvider = z.infer<typeof SearchProvider>;
|
|
33
|
+
/** Run id format: `RES-YYYY-MM-DD-<8 hex>` (research) or `PRD-...` (prd). */
|
|
34
|
+
export declare const RunId: z.ZodString;
|
|
35
|
+
export type RunId = z.infer<typeof RunId>;
|
|
36
|
+
/** SHA-256 hex digest (64 chars). */
|
|
37
|
+
export declare const Sha256: z.ZodString;
|
|
38
|
+
export type Sha256 = z.infer<typeof Sha256>;
|
|
39
|
+
/** Git commit SHA (7-40 hex chars). */
|
|
40
|
+
export declare const GitSha: z.ZodString;
|
|
41
|
+
export type GitSha = z.infer<typeof GitSha>;
|
|
42
|
+
/** ISO-8601 timestamp. We accept anything Date.parse can handle. */
|
|
43
|
+
export declare const IsoTimestamp: z.ZodEffects<z.ZodString, string, string>;
|
|
44
|
+
export type IsoTimestamp = z.infer<typeof IsoTimestamp>;
|
|
45
|
+
/** Confidence rating attached to research conclusions. */
|
|
46
|
+
export declare const Confidence: z.ZodEnum<["HIGH", "MEDIUM", "LOW"]>;
|
|
47
|
+
export type Confidence = z.infer<typeof Confidence>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Confidence = exports.IsoTimestamp = exports.GitSha = exports.Sha256 = exports.RunId = exports.SearchProvider = exports.PrdMode = exports.ResearchPath = exports.ScopeLevel = exports.LlmProvider = exports.GroundingMode = exports.GuardrailMode = void 0;
|
|
4
|
+
exports.providerNeedsApiKey = providerNeedsApiKey;
|
|
5
|
+
/**
|
|
6
|
+
* Shared primitive enums + branded types used across every other schema.
|
|
7
|
+
* Importing from here keeps the surface coherent and lets downstream tests
|
|
8
|
+
* assert against canonical literal unions instead of free strings.
|
|
9
|
+
*/
|
|
10
|
+
const zod_1 = require("zod");
|
|
11
|
+
exports.GuardrailMode = zod_1.z.enum(['strict', 'default', 'lenient']);
|
|
12
|
+
exports.GroundingMode = zod_1.z.enum(['strict', 'default', 'lenient']);
|
|
13
|
+
exports.LlmProvider = zod_1.z.enum(['anthropic', 'openai', 'azure-openai', 'github-models']);
|
|
14
|
+
/**
|
|
15
|
+
* Providers that route through GitHub Models / Copilot use the workflow's
|
|
16
|
+
* GITHUB_TOKEN with `permissions: models: read` — no separate API key needs
|
|
17
|
+
* to live as a repo secret.
|
|
18
|
+
*/
|
|
19
|
+
function providerNeedsApiKey(p) {
|
|
20
|
+
return p !== 'github-models';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Research / PRD scope. Portfolio scope was dropped — too abstract for
|
|
24
|
+
* the agents to produce a targeted PRD. A run must be anchored to a
|
|
25
|
+
* concrete architectural surface: a platform (multiple BARs sharing a
|
|
26
|
+
* CALM model) or a single BAR.
|
|
27
|
+
*/
|
|
28
|
+
exports.ScopeLevel = zod_1.z.enum(['platform', 'bar']);
|
|
29
|
+
exports.ResearchPath = zod_1.z.enum(['research', 'archaeology']);
|
|
30
|
+
exports.PrdMode = zod_1.z.enum(['shallow', 'deep']);
|
|
31
|
+
exports.SearchProvider = zod_1.z.enum(['tavily', 'arxiv', 'uspto', 'hackernews']);
|
|
32
|
+
/** Run id format: `RES-YYYY-MM-DD-<8 hex>` (research) or `PRD-...` (prd). */
|
|
33
|
+
exports.RunId = zod_1.z.string().regex(/^(RES|PRD)-\d{4}-\d{2}-\d{2}-[0-9a-f]{8}$/);
|
|
34
|
+
/** SHA-256 hex digest (64 chars). */
|
|
35
|
+
exports.Sha256 = zod_1.z.string().regex(/^[0-9a-f]{64}$/);
|
|
36
|
+
/** Git commit SHA (7-40 hex chars). */
|
|
37
|
+
exports.GitSha = zod_1.z.string().regex(/^[0-9a-f]{7,40}$/);
|
|
38
|
+
/** ISO-8601 timestamp. We accept anything Date.parse can handle. */
|
|
39
|
+
exports.IsoTimestamp = zod_1.z.string().refine(v => !Number.isNaN(Date.parse(v)), { message: 'Must be an ISO-8601 timestamp parseable by Date.parse' });
|
|
40
|
+
/** Confidence rating attached to research conclusions. */
|
|
41
|
+
exports.Confidence = zod_1.z.enum(['HIGH', 'MEDIUM', 'LOW']);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryPlan — per-provider query specialisation produced by `plan_queries`.
|
|
3
|
+
*
|
|
4
|
+
* Counts and per-provider rules are STRICT (the design doc's "Why this format
|
|
5
|
+
* choice" section: each search backend reads queries differently and one
|
|
6
|
+
* generic query plan gets sub-optimal recall everywhere).
|
|
7
|
+
*
|
|
8
|
+
* Web queries MUST include the current year for recency anchoring; arxiv
|
|
9
|
+
* queries are short technical phrases (3-6 words); patent queries use AND
|
|
10
|
+
* operators; community queries are casual 2-3 word HN-style phrases.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
export declare const QueryPlan: z.ZodObject<{
|
|
14
|
+
/** Exactly 5 natural-language web queries; each must contain a 4-digit year. */
|
|
15
|
+
web: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
|
|
16
|
+
/** Exactly 3 short technical phrases for arXiv. */
|
|
17
|
+
arxiv: z.ZodArray<z.ZodString, "many">;
|
|
18
|
+
/** Exactly 3 USPTO queries — keyword sets joined with AND. */
|
|
19
|
+
patent: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
|
|
20
|
+
/** Exactly 3 casual HackerNews-style phrases. */
|
|
21
|
+
community: z.ZodArray<z.ZodString, "many">;
|
|
22
|
+
}, "strip", z.ZodTypeAny, {
|
|
23
|
+
arxiv: string[];
|
|
24
|
+
web: string[];
|
|
25
|
+
patent: string[];
|
|
26
|
+
community: string[];
|
|
27
|
+
}, {
|
|
28
|
+
arxiv: string[];
|
|
29
|
+
web: string[];
|
|
30
|
+
patent: string[];
|
|
31
|
+
community: string[];
|
|
32
|
+
}>;
|
|
33
|
+
export type QueryPlan = z.infer<typeof QueryPlan>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryPlan = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* QueryPlan — per-provider query specialisation produced by `plan_queries`.
|
|
6
|
+
*
|
|
7
|
+
* Counts and per-provider rules are STRICT (the design doc's "Why this format
|
|
8
|
+
* choice" section: each search backend reads queries differently and one
|
|
9
|
+
* generic query plan gets sub-optimal recall everywhere).
|
|
10
|
+
*
|
|
11
|
+
* Web queries MUST include the current year for recency anchoring; arxiv
|
|
12
|
+
* queries are short technical phrases (3-6 words); patent queries use AND
|
|
13
|
+
* operators; community queries are casual 2-3 word HN-style phrases.
|
|
14
|
+
*/
|
|
15
|
+
const zod_1 = require("zod");
|
|
16
|
+
exports.QueryPlan = zod_1.z.object({
|
|
17
|
+
/** Exactly 5 natural-language web queries; each must contain a 4-digit year. */
|
|
18
|
+
web: zod_1.z.array(zod_1.z.string().min(3).refine(v => /\b(19|20|21)\d{2}\b/.test(v), { message: 'Web query must contain a 4-digit year for recency anchoring' })).length(5),
|
|
19
|
+
/** Exactly 3 short technical phrases for arXiv. */
|
|
20
|
+
arxiv: zod_1.z.array(zod_1.z.string().min(3).max(80)).length(3),
|
|
21
|
+
/** Exactly 3 USPTO queries — keyword sets joined with AND. */
|
|
22
|
+
patent: zod_1.z.array(zod_1.z.string().min(3).refine(v => /\bAND\b/.test(v), { message: 'Patent query must join keywords with explicit AND operators' })).length(3),
|
|
23
|
+
/** Exactly 3 casual HackerNews-style phrases. */
|
|
24
|
+
community: zod_1.z.array(zod_1.z.string().min(2).max(40)).length(3),
|
|
25
|
+
});
|