@wooojin/forgen 0.4.10 → 0.4.12
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +62 -0
- package/README.md +33 -1
- package/assets/claude/agents/forgen-verify.md +65 -0
- package/assets/claude/workflows/compound-extract.js +136 -0
- package/assets/claude/workflows/evidence-gate-audit.js +107 -0
- package/assets/shared/hook-registry.json +1 -0
- package/dist/checks/_shared/meta-guard-dispatch.d.ts +38 -0
- package/dist/checks/_shared/meta-guard-dispatch.js +80 -0
- package/dist/checks/_shared/text-sanitizer.js +15 -0
- package/dist/cli.js +57 -2
- package/dist/core/changelog-cli.d.ts +7 -0
- package/dist/core/changelog-cli.js +100 -0
- package/dist/core/doctor.d.ts +3 -0
- package/dist/core/doctor.js +38 -0
- package/dist/core/effort-advisory.d.ts +23 -0
- package/dist/core/effort-advisory.js +29 -0
- package/dist/core/explain-cli.d.ts +6 -0
- package/dist/core/explain-cli.js +99 -0
- package/dist/core/health-cli.d.ts +23 -0
- package/dist/core/health-cli.js +86 -0
- package/dist/core/probe-workflow-cli.d.ts +72 -0
- package/dist/core/probe-workflow-cli.js +282 -0
- package/dist/core/spawn.d.ts +13 -0
- package/dist/core/spawn.js +36 -8
- package/dist/core/stats-cli.d.ts +22 -9
- package/dist/core/stats-cli.js +149 -0
- package/dist/core/watch-cli.d.ts +7 -0
- package/dist/core/watch-cli.js +185 -0
- package/dist/core/workflows-cli.d.ts +26 -0
- package/dist/core/workflows-cli.js +120 -0
- package/dist/engine/compound-export.d.ts +12 -0
- package/dist/engine/compound-export.js +136 -14
- package/dist/engine/compound-extractor.d.ts +12 -43
- package/dist/engine/compound-extractor.js +27 -756
- package/dist/engine/extraction-diff.d.ts +11 -0
- package/dist/engine/extraction-diff.js +105 -0
- package/dist/engine/extraction-gates.d.ts +37 -0
- package/dist/engine/extraction-gates.js +100 -0
- package/dist/engine/extraction-git.d.ts +20 -0
- package/dist/engine/extraction-git.js +75 -0
- package/dist/engine/extraction-persistence.d.ts +27 -0
- package/dist/engine/extraction-persistence.js +140 -0
- package/dist/engine/extraction-session.d.ts +26 -0
- package/dist/engine/extraction-session.js +230 -0
- package/dist/engine/lifecycle/types.d.ts +1 -1
- package/dist/engine/meta-learning/matcher-weight-loader.d.ts +16 -0
- package/dist/engine/meta-learning/matcher-weight-loader.js +45 -0
- package/dist/engine/precision-guards.d.ts +14 -0
- package/dist/engine/precision-guards.js +39 -0
- package/dist/engine/ranking-pipeline.d.ts +45 -0
- package/dist/engine/ranking-pipeline.js +66 -0
- package/dist/engine/relevance-scorer.d.ts +43 -0
- package/dist/engine/relevance-scorer.js +81 -0
- package/dist/engine/scoring-algorithms.d.ts +31 -0
- package/dist/engine/scoring-algorithms.js +109 -0
- package/dist/engine/solution-matcher-eval.d.ts +97 -0
- package/dist/engine/solution-matcher-eval.js +122 -0
- package/dist/engine/solution-matcher.d.ts +21 -380
- package/dist/engine/solution-matcher.js +27 -828
- package/dist/fgx.js +1 -1
- package/dist/hooks/notepad-injector.js +7 -0
- package/dist/hooks/post-tool-use.js +8 -1
- package/dist/hooks/secret-filter.d.ts +1 -0
- package/dist/hooks/secret-filter.js +17 -7
- package/dist/hooks/shared/preflight-check.d.ts +15 -0
- package/dist/hooks/shared/preflight-check.js +51 -0
- package/dist/hooks/stop-guard.js +19 -60
- package/dist/hooks/subagent-stop-guard.d.ts +23 -0
- package/dist/hooks/subagent-stop-guard.js +158 -0
- package/dist/hooks/subagent-tracker.d.ts +36 -3
- package/dist/hooks/subagent-tracker.js +86 -39
- package/hooks/hooks.json +6 -1
- package/package.json +7 -7
- package/plugin.json +1 -1
- package/scripts/postinstall.js +10 -7
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless scoring primitives for the solution matcher.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from solution-matcher.ts — these are pure functions with no
|
|
5
|
+
* filesystem or module-level state dependencies.
|
|
6
|
+
*/
|
|
7
|
+
/** High-frequency tags that should be weighted lower */
|
|
8
|
+
export const COMMON_TAGS = new Set([
|
|
9
|
+
'typescript',
|
|
10
|
+
'ts',
|
|
11
|
+
'javascript',
|
|
12
|
+
'js',
|
|
13
|
+
'fix',
|
|
14
|
+
'update',
|
|
15
|
+
'add',
|
|
16
|
+
'change',
|
|
17
|
+
'file',
|
|
18
|
+
'code',
|
|
19
|
+
'function',
|
|
20
|
+
'import',
|
|
21
|
+
'export',
|
|
22
|
+
'error',
|
|
23
|
+
'type',
|
|
24
|
+
'string',
|
|
25
|
+
'number',
|
|
26
|
+
'object',
|
|
27
|
+
'array',
|
|
28
|
+
'return',
|
|
29
|
+
'const',
|
|
30
|
+
'class',
|
|
31
|
+
'module',
|
|
32
|
+
'코드',
|
|
33
|
+
'파일',
|
|
34
|
+
'함수',
|
|
35
|
+
'수정',
|
|
36
|
+
'추가',
|
|
37
|
+
'변경',
|
|
38
|
+
'에러',
|
|
39
|
+
'타입',
|
|
40
|
+
]);
|
|
41
|
+
/** Apply IDF-like weight: common tags get reduced weight */
|
|
42
|
+
export function tagWeight(tag) {
|
|
43
|
+
return COMMON_TAGS.has(tag) ? 0.5 : 1.0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compute the Dice coefficient between two strings using character bigrams.
|
|
47
|
+
*
|
|
48
|
+
* Dice = 2 * |intersection| / (|A| + |B|)
|
|
49
|
+
*
|
|
50
|
+
* Both strings are lowercased and whitespace-stripped before bigram generation.
|
|
51
|
+
* Returns 0 for empty strings or single-character strings (no bigrams possible).
|
|
52
|
+
* Returns 1.0 for identical non-trivial strings.
|
|
53
|
+
*
|
|
54
|
+
* This is used as a lightweight fuzzy matching signal for borderline cases
|
|
55
|
+
* where the TF-IDF tag intersection produces a low score but the query and
|
|
56
|
+
* solution tags are character-similar (e.g., "database" vs "데이터베이스"
|
|
57
|
+
* won't match, but "database" vs "databse" will get a high score).
|
|
58
|
+
*/
|
|
59
|
+
export function bigramSimilarity(a, b) {
|
|
60
|
+
const na = a.toLowerCase().replace(/\s+/g, '');
|
|
61
|
+
const nb = b.toLowerCase().replace(/\s+/g, '');
|
|
62
|
+
if (na.length < 2 || nb.length < 2)
|
|
63
|
+
return 0;
|
|
64
|
+
if (na === nb)
|
|
65
|
+
return 1.0;
|
|
66
|
+
const bigramsA = new Map();
|
|
67
|
+
for (let i = 0; i < na.length - 1; i++) {
|
|
68
|
+
const bg = na.slice(i, i + 2);
|
|
69
|
+
bigramsA.set(bg, (bigramsA.get(bg) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
const bigramsB = new Map();
|
|
72
|
+
for (let i = 0; i < nb.length - 1; i++) {
|
|
73
|
+
const bg = nb.slice(i, i + 2);
|
|
74
|
+
bigramsB.set(bg, (bigramsB.get(bg) ?? 0) + 1);
|
|
75
|
+
}
|
|
76
|
+
let intersectionSize = 0;
|
|
77
|
+
for (const [bg, countA] of bigramsA) {
|
|
78
|
+
const countB = bigramsB.get(bg) ?? 0;
|
|
79
|
+
intersectionSize += Math.min(countA, countB);
|
|
80
|
+
}
|
|
81
|
+
const totalA = na.length - 1;
|
|
82
|
+
const totalB = nb.length - 1;
|
|
83
|
+
return (2 * intersectionSize) / (totalA + totalB);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Simplified BM25 score for a single query-document pair.
|
|
87
|
+
* Uses tag overlap with term frequency normalization.
|
|
88
|
+
* k1=1.2, b=0.75 (standard BM25 parameters).
|
|
89
|
+
*/
|
|
90
|
+
export function bm25Score(queryTags, docTags, avgDocLength) {
|
|
91
|
+
const k1 = 1.2;
|
|
92
|
+
const b = 0.75;
|
|
93
|
+
const docLen = docTags.length;
|
|
94
|
+
if (docLen === 0 || queryTags.length === 0 || avgDocLength === 0)
|
|
95
|
+
return 0;
|
|
96
|
+
let score = 0;
|
|
97
|
+
for (const qt of queryTags) {
|
|
98
|
+
// Term frequency in document
|
|
99
|
+
const tf = docTags.filter((dt) => dt === qt || (dt.length > 3 && qt.length > 3 && (dt.includes(qt) || qt.includes(dt)))).length;
|
|
100
|
+
if (tf === 0)
|
|
101
|
+
continue;
|
|
102
|
+
// BM25 TF saturation
|
|
103
|
+
const numerator = tf * (k1 + 1);
|
|
104
|
+
const denominator = tf + k1 * (1 - b + b * (docLen / avgDocLength));
|
|
105
|
+
score += numerator / denominator;
|
|
106
|
+
}
|
|
107
|
+
// Normalize by query length
|
|
108
|
+
return score / queryTags.length;
|
|
109
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap evaluator + IR metrics for the solution matcher.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from solution-matcher.ts — test/CI infrastructure for measuring
|
|
5
|
+
* matcher quality against labeled fixtures. No production side-effects.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* In-memory solution shape for the bootstrap evaluator. Mirrors the index
|
|
9
|
+
* entry fields that `matchSolutions` consumes (tags, identifiers, confidence)
|
|
10
|
+
* but without any filesystem dependency.
|
|
11
|
+
*/
|
|
12
|
+
export interface EvalSolution {
|
|
13
|
+
name: string;
|
|
14
|
+
tags: string[];
|
|
15
|
+
identifiers?: string[];
|
|
16
|
+
confidence: number;
|
|
17
|
+
}
|
|
18
|
+
export interface EvalQuery {
|
|
19
|
+
query: string;
|
|
20
|
+
/** Names that should appear in the top-5. Empty array = expect no match (negative case). */
|
|
21
|
+
expectAnyOf: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface EvalFixture {
|
|
24
|
+
solutions: EvalSolution[];
|
|
25
|
+
positive: EvalQuery[];
|
|
26
|
+
/** Bilingual or compound-word variants that exercise synonym expansion. */
|
|
27
|
+
paraphrase: EvalQuery[];
|
|
28
|
+
/** Unrelated queries that should not return a top-1 hit. */
|
|
29
|
+
negative: EvalQuery[];
|
|
30
|
+
}
|
|
31
|
+
/** Per-bucket metrics. Paraphrase and positive are reported separately so a
|
|
32
|
+
* bilingual regression (T2 synonym change) can't hide inside the aggregate. */
|
|
33
|
+
export interface BucketMetrics {
|
|
34
|
+
/** |{q : ∃i≤5, ranked[i] ∈ q.expectAnyOf}| / |q| */
|
|
35
|
+
recallAt5: number;
|
|
36
|
+
/** Σ (1 / firstMatchRank) / |q|; rank > 5 contributes 0. */
|
|
37
|
+
mrrAt5: number;
|
|
38
|
+
/** |{q : ranked is empty}| / |q| */
|
|
39
|
+
noResultRate: number;
|
|
40
|
+
/** Number of queries in this bucket. */
|
|
41
|
+
total: number;
|
|
42
|
+
}
|
|
43
|
+
export interface EvalResult {
|
|
44
|
+
/** Combined (positive ∪ paraphrase) metrics — backwards-compatible headline numbers. */
|
|
45
|
+
recallAt5: number;
|
|
46
|
+
mrrAt5: number;
|
|
47
|
+
noResultRate: number;
|
|
48
|
+
/**
|
|
49
|
+
* Fraction of negative queries where the matcher returned ≥ 1 candidate
|
|
50
|
+
* (regardless of rank).
|
|
51
|
+
*/
|
|
52
|
+
negativeAnyResultRate: number;
|
|
53
|
+
/** Per-bucket breakdown — use these to catch paraphrase-only regressions. */
|
|
54
|
+
byBucket: {
|
|
55
|
+
positive: BucketMetrics;
|
|
56
|
+
paraphrase: BucketMetrics;
|
|
57
|
+
};
|
|
58
|
+
total: {
|
|
59
|
+
positive: number;
|
|
60
|
+
paraphrase: number;
|
|
61
|
+
negative: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Round 3 baseline metrics, recorded against the current `term-normalizer`
|
|
66
|
+
* + `calculateRelevance` + fixture `solution-match-bootstrap.json`. Used as
|
|
67
|
+
* a relative regression guard in `tests/solution-matcher-eval.test.ts` —
|
|
68
|
+
* downstream PRs must not regress any field by more than `BASELINE_TOLERANCE`.
|
|
69
|
+
*
|
|
70
|
+
* History (chronological ascending — v1 at top, latest at bottom):
|
|
71
|
+
* - v1 (2026-04-08, fixture v1, 41+10+10 queries): 1.0 / 1.0 / 0.0 / 0.1
|
|
72
|
+
* - v2 (2026-04-08, fixture v2, 53+16+14 queries): 1.0 / 0.969 / 0.0 / 0.357
|
|
73
|
+
* - v3 (2026-04-08, fixture v2 + R4-T1 compound-tag fix): 1.0 / 0.986 / 0.0 / 0.357
|
|
74
|
+
* - v4 (2026-04-08, fixture v2 + R4-T1 + R4-T2 phrase blocklist): 1.0 / 0.986 / 0.0 / 0.143
|
|
75
|
+
* - v5 (2026-04-08, fixture v2 + R4-T1 + R4-T2 + R4-T3 specificity guards): 1.0 / 0.986 / 0.0 / 0.000
|
|
76
|
+
*
|
|
77
|
+
* If a PR legitimately improves a metric, update this constant in the same
|
|
78
|
+
* commit so future PRs guard against the new floor.
|
|
79
|
+
*/
|
|
80
|
+
export declare const ROUND3_BASELINE: EvalResult;
|
|
81
|
+
/** Maximum allowed absolute regression per metric. */
|
|
82
|
+
export declare const BASELINE_TOLERANCE = 0.05;
|
|
83
|
+
/**
|
|
84
|
+
* Test/diagnostic helper: evaluate one query against a fixture solution set
|
|
85
|
+
* and return the top-5 ranked candidates with their relevance + matched tags.
|
|
86
|
+
*/
|
|
87
|
+
export declare function evaluateQuery(query: string, solutions: readonly EvalSolution[]): Array<{
|
|
88
|
+
name: string;
|
|
89
|
+
relevance: number;
|
|
90
|
+
matchedTags: string[];
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Evaluate the current matcher against a labeled fixture and return IR
|
|
94
|
+
* metrics. Uses `rankCandidates` (shared with `matchSolutions`) so the
|
|
95
|
+
* evaluator can't silently drift from production ranking behaviour.
|
|
96
|
+
*/
|
|
97
|
+
export declare function evaluateSolutionMatcher(fixture: EvalFixture): EvalResult;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap evaluator + IR metrics for the solution matcher.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from solution-matcher.ts — test/CI infrastructure for measuring
|
|
5
|
+
* matcher quality against labeled fixtures. No production side-effects.
|
|
6
|
+
*/
|
|
7
|
+
import { extractTags } from './solution-format.js';
|
|
8
|
+
import { rankCandidates } from './ranking-pipeline.js';
|
|
9
|
+
/**
|
|
10
|
+
* Round 3 baseline metrics, recorded against the current `term-normalizer`
|
|
11
|
+
* + `calculateRelevance` + fixture `solution-match-bootstrap.json`. Used as
|
|
12
|
+
* a relative regression guard in `tests/solution-matcher-eval.test.ts` —
|
|
13
|
+
* downstream PRs must not regress any field by more than `BASELINE_TOLERANCE`.
|
|
14
|
+
*
|
|
15
|
+
* History (chronological ascending — v1 at top, latest at bottom):
|
|
16
|
+
* - v1 (2026-04-08, fixture v1, 41+10+10 queries): 1.0 / 1.0 / 0.0 / 0.1
|
|
17
|
+
* - v2 (2026-04-08, fixture v2, 53+16+14 queries): 1.0 / 0.969 / 0.0 / 0.357
|
|
18
|
+
* - v3 (2026-04-08, fixture v2 + R4-T1 compound-tag fix): 1.0 / 0.986 / 0.0 / 0.357
|
|
19
|
+
* - v4 (2026-04-08, fixture v2 + R4-T1 + R4-T2 phrase blocklist): 1.0 / 0.986 / 0.0 / 0.143
|
|
20
|
+
* - v5 (2026-04-08, fixture v2 + R4-T1 + R4-T2 + R4-T3 specificity guards): 1.0 / 0.986 / 0.0 / 0.000
|
|
21
|
+
*
|
|
22
|
+
* If a PR legitimately improves a metric, update this constant in the same
|
|
23
|
+
* commit so future PRs guard against the new floor.
|
|
24
|
+
*/
|
|
25
|
+
export const ROUND3_BASELINE = {
|
|
26
|
+
recallAt5: 1.0,
|
|
27
|
+
mrrAt5: 0.986,
|
|
28
|
+
noResultRate: 0.0,
|
|
29
|
+
negativeAnyResultRate: 0.0,
|
|
30
|
+
byBucket: {
|
|
31
|
+
positive: { recallAt5: 1.0, mrrAt5: 0.981, noResultRate: 0.0, total: 53 },
|
|
32
|
+
paraphrase: { recallAt5: 1.0, mrrAt5: 1.0, noResultRate: 0.0, total: 16 },
|
|
33
|
+
},
|
|
34
|
+
total: { positive: 53, paraphrase: 16, negative: 14 },
|
|
35
|
+
};
|
|
36
|
+
/** Maximum allowed absolute regression per metric. */
|
|
37
|
+
export const BASELINE_TOLERANCE = 0.05;
|
|
38
|
+
/** Run a single bucket through the ranking pipeline and aggregate IR metrics. */
|
|
39
|
+
function computeBucketMetrics(queries, solutions) {
|
|
40
|
+
let recallHits = 0;
|
|
41
|
+
let reciprocalSum = 0;
|
|
42
|
+
let noResultCount = 0;
|
|
43
|
+
for (const q of queries) {
|
|
44
|
+
const promptTags = extractTags(q.query);
|
|
45
|
+
const ranked = rankCandidates(promptTags, q.query.toLowerCase(), solutions);
|
|
46
|
+
if (ranked.length === 0) {
|
|
47
|
+
noResultCount++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
51
|
+
if (q.expectAnyOf.includes(ranked[i].solution.name)) {
|
|
52
|
+
recallHits++;
|
|
53
|
+
reciprocalSum += 1 / (i + 1);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const total = queries.length;
|
|
59
|
+
return {
|
|
60
|
+
recallAt5: total > 0 ? recallHits / total : 0,
|
|
61
|
+
mrrAt5: total > 0 ? reciprocalSum / total : 0,
|
|
62
|
+
noResultRate: total > 0 ? noResultCount / total : 0,
|
|
63
|
+
total,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Test/diagnostic helper: evaluate one query against a fixture solution set
|
|
68
|
+
* and return the top-5 ranked candidates with their relevance + matched tags.
|
|
69
|
+
*/
|
|
70
|
+
export function evaluateQuery(query, solutions) {
|
|
71
|
+
const promptTags = extractTags(query);
|
|
72
|
+
return rankCandidates(promptTags, query.toLowerCase(), solutions).map((c) => ({
|
|
73
|
+
name: c.solution.name,
|
|
74
|
+
relevance: c.relevance,
|
|
75
|
+
matchedTags: c.matchedTags,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Evaluate the current matcher against a labeled fixture and return IR
|
|
80
|
+
* metrics. Uses `rankCandidates` (shared with `matchSolutions`) so the
|
|
81
|
+
* evaluator can't silently drift from production ranking behaviour.
|
|
82
|
+
*/
|
|
83
|
+
export function evaluateSolutionMatcher(fixture) {
|
|
84
|
+
const positiveM = computeBucketMetrics(fixture.positive, fixture.solutions);
|
|
85
|
+
const paraphraseM = computeBucketMetrics(fixture.paraphrase, fixture.solutions);
|
|
86
|
+
const combinedTotal = positiveM.total + paraphraseM.total;
|
|
87
|
+
const recallAt5 = combinedTotal > 0
|
|
88
|
+
? (positiveM.recallAt5 * positiveM.total + paraphraseM.recallAt5 * paraphraseM.total) /
|
|
89
|
+
combinedTotal
|
|
90
|
+
: 0;
|
|
91
|
+
const mrrAt5 = combinedTotal > 0
|
|
92
|
+
? (positiveM.mrrAt5 * positiveM.total + paraphraseM.mrrAt5 * paraphraseM.total) /
|
|
93
|
+
combinedTotal
|
|
94
|
+
: 0;
|
|
95
|
+
const noResultRate = combinedTotal > 0
|
|
96
|
+
? (positiveM.noResultRate * positiveM.total + paraphraseM.noResultRate * paraphraseM.total) /
|
|
97
|
+
combinedTotal
|
|
98
|
+
: 0;
|
|
99
|
+
let negAnyResult = 0;
|
|
100
|
+
for (const q of fixture.negative) {
|
|
101
|
+
const promptTags = extractTags(q.query);
|
|
102
|
+
const ranked = rankCandidates(promptTags, q.query.toLowerCase(), fixture.solutions);
|
|
103
|
+
if (ranked.length >= 1)
|
|
104
|
+
negAnyResult++;
|
|
105
|
+
}
|
|
106
|
+
const negTotal = fixture.negative.length;
|
|
107
|
+
return {
|
|
108
|
+
recallAt5,
|
|
109
|
+
mrrAt5,
|
|
110
|
+
noResultRate,
|
|
111
|
+
negativeAnyResultRate: negTotal > 0 ? negAnyResult / negTotal : 0,
|
|
112
|
+
byBucket: {
|
|
113
|
+
positive: positiveM,
|
|
114
|
+
paraphrase: paraphraseM,
|
|
115
|
+
},
|
|
116
|
+
total: {
|
|
117
|
+
positive: fixture.positive.length,
|
|
118
|
+
paraphrase: fixture.paraphrase.length,
|
|
119
|
+
negative: fixture.negative.length,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|