@qulib/core 0.2.1 → 0.3.0
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/README.md +45 -3
- package/dist/analyze.d.ts +16 -4
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +98 -38
- package/dist/cli/cost-doctor.d.ts +2 -0
- package/dist/cli/cost-doctor.d.ts.map +1 -0
- package/dist/cli/cost-doctor.js +72 -0
- package/dist/cli/index.js +61 -0
- package/dist/harness/progress-log.d.ts +7 -0
- package/dist/harness/progress-log.d.ts.map +1 -0
- package/dist/harness/progress-log.js +1 -0
- package/dist/harness/run-options.d.ts +2 -0
- package/dist/harness/run-options.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/llm/content-hash.d.ts +2 -0
- package/dist/llm/content-hash.d.ts.map +1 -0
- package/dist/llm/content-hash.js +4 -0
- package/dist/llm/context-builder.js +1 -1
- package/dist/llm/cost-intelligence.d.ts +29 -0
- package/dist/llm/cost-intelligence.d.ts.map +1 -0
- package/dist/llm/cost-intelligence.js +153 -0
- package/dist/llm/provider.d.ts +11 -1
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +43 -4
- package/dist/phases/act.d.ts.map +1 -1
- package/dist/phases/act.js +4 -1
- package/dist/phases/observe.js +1 -1
- package/dist/phases/think-finalize.d.ts +6 -0
- package/dist/phases/think-finalize.d.ts.map +1 -0
- package/dist/phases/think-finalize.js +164 -0
- package/dist/phases/think.d.ts +2 -0
- package/dist/phases/think.d.ts.map +1 -1
- package/dist/phases/think.js +16 -65
- package/dist/reporters/markdown-reporter.d.ts.map +1 -1
- package/dist/reporters/markdown-reporter.js +23 -3
- package/dist/schemas/config.schema.d.ts +364 -0
- package/dist/schemas/config.schema.d.ts.map +1 -1
- package/dist/schemas/config.schema.js +55 -1
- package/dist/schemas/cost-intelligence.schema.d.ts +229 -0
- package/dist/schemas/cost-intelligence.schema.d.ts.map +1 -0
- package/dist/schemas/cost-intelligence.schema.js +41 -0
- package/dist/schemas/decision-log.schema.d.ts +2 -2
- package/dist/schemas/gap-analysis.schema.d.ts +288 -49
- package/dist/schemas/gap-analysis.schema.d.ts.map +1 -1
- package/dist/schemas/gap-analysis.schema.js +7 -3
- package/dist/schemas/index.d.ts +3 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +3 -1
- package/dist/schemas/public-surface.schema.d.ts +268 -0
- package/dist/schemas/public-surface.schema.d.ts.map +1 -0
- package/dist/schemas/public-surface.schema.js +15 -0
- package/dist/schemas/repo-analysis.schema.d.ts +6 -6
- package/dist/tools/auth-block-gap.d.ts +3 -0
- package/dist/tools/auth-block-gap.d.ts.map +1 -0
- package/dist/tools/auth-block-gap.js +19 -0
- package/dist/tools/auth-detector.d.ts +2 -1
- package/dist/tools/auth-detector.d.ts.map +1 -1
- package/dist/tools/auth-detector.js +28 -3
- package/dist/tools/auth-explorer.d.ts +4 -0
- package/dist/tools/auth-explorer.d.ts.map +1 -0
- package/dist/tools/auth-explorer.js +346 -0
- package/dist/tools/auth-surface-analyzer.d.ts +4 -0
- package/dist/tools/auth-surface-analyzer.d.ts.map +1 -0
- package/dist/tools/auth-surface-analyzer.js +154 -0
- package/dist/tools/cypress-explorer.d.ts +2 -1
- package/dist/tools/cypress-explorer.d.ts.map +1 -1
- package/dist/tools/cypress-explorer.js +1 -1
- package/dist/tools/explorer.interface.d.ts +2 -1
- package/dist/tools/explorer.interface.d.ts.map +1 -1
- package/dist/tools/gap-engine.d.ts +3 -1
- package/dist/tools/gap-engine.d.ts.map +1 -1
- package/dist/tools/gap-engine.js +39 -12
- package/dist/tools/oauth-providers.d.ts +7 -0
- package/dist/tools/oauth-providers.d.ts.map +1 -0
- package/dist/tools/oauth-providers.js +21 -0
- package/dist/tools/playwright-explorer.d.ts +2 -1
- package/dist/tools/playwright-explorer.d.ts.map +1 -1
- package/dist/tools/playwright-explorer.js +21 -3
- package/dist/tools/public-surface.d.ts +5 -0
- package/dist/tools/public-surface.d.ts.map +1 -0
- package/dist/tools/public-surface.js +13 -0
- package/dist/tools/user-providers.d.ts +15 -0
- package/dist/tools/user-providers.d.ts.map +1 -0
- package/dist/tools/user-providers.js +62 -0
- package/package.json +6 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export function summarizeUsageQuality(records) {
|
|
2
|
+
if (records.length === 0) {
|
|
3
|
+
return { totalInputTokens: 0, totalOutputTokens: 0, dataQuality: 'none' };
|
|
4
|
+
}
|
|
5
|
+
const totalInputTokens = records.reduce((s, r) => s + r.inputTokens, 0);
|
|
6
|
+
const totalOutputTokens = records.reduce((s, r) => s + r.outputTokens, 0);
|
|
7
|
+
const qualities = new Set(records.map((r) => r.dataQuality));
|
|
8
|
+
if (qualities.size > 1) {
|
|
9
|
+
return { totalInputTokens, totalOutputTokens, dataQuality: 'mixed' };
|
|
10
|
+
}
|
|
11
|
+
const only = records[0].dataQuality;
|
|
12
|
+
return { totalInputTokens, totalOutputTokens, dataQuality: only };
|
|
13
|
+
}
|
|
14
|
+
export function buildBudgetWarnings(records, maxOutputTokensPerLlmCall) {
|
|
15
|
+
const warnings = [];
|
|
16
|
+
for (const r of records) {
|
|
17
|
+
if (r.dataQuality === 'none' && r.inputTokens === 0 && r.outputTokens === 0) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const out = r.outputTokens;
|
|
21
|
+
if (out >= maxOutputTokensPerLlmCall) {
|
|
22
|
+
warnings.push(`Output tokens (${out}) reached or exceeded the configured per-completion max-output ceiling (${maxOutputTokensPerLlmCall}). Completion may be truncated; raise llmMaxOutputTokensPerCall (or legacy llmTokenBudget) or reduce testGenerationLimit.`);
|
|
23
|
+
}
|
|
24
|
+
else if (out >= Math.floor(maxOutputTokensPerLlmCall * 0.8)) {
|
|
25
|
+
warnings.push(`Output tokens (${out}) are within 80% of the per-completion max-output ceiling (${maxOutputTokensPerLlmCall}). Truncation risk on heavier prompts.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (records.some((r) => r.dataQuality === 'estimated')) {
|
|
29
|
+
warnings.push('Some token counts are estimated (API usage missing or call failed). Treat totals as approximate until actual usage is available.');
|
|
30
|
+
}
|
|
31
|
+
return warnings;
|
|
32
|
+
}
|
|
33
|
+
export function findRepeatedPromptPatterns(records) {
|
|
34
|
+
const byHash = new Map();
|
|
35
|
+
for (const r of records) {
|
|
36
|
+
if (!r.promptHash)
|
|
37
|
+
continue;
|
|
38
|
+
byHash.set(r.promptHash, (byHash.get(r.promptHash) ?? 0) + 1);
|
|
39
|
+
}
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const [promptHash, count] of byHash) {
|
|
42
|
+
if (count < 2)
|
|
43
|
+
continue;
|
|
44
|
+
out.push({
|
|
45
|
+
promptHash,
|
|
46
|
+
count,
|
|
47
|
+
recommendation: 'The same prompt fingerprint appeared multiple times in this run. Capture the intent as versioned Playwright/Cypress checks or shared scenario fixtures so repeat visits do not re-spend model tokens.',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export function buildConversionRecommendations(params) {
|
|
53
|
+
const rec = [];
|
|
54
|
+
if (params.scenarioSource === 'llm' && params.gapCount > 0) {
|
|
55
|
+
rec.push('LLM-authored scenarios are suggestions only. Promote the highest-severity paths into deterministic checks (axe, link crawl, route smoke) checked on every deploy to shrink future LLM reliance.');
|
|
56
|
+
}
|
|
57
|
+
if (params.repeatedOperations.length > 0) {
|
|
58
|
+
rec.push('Deduplicate repeated AI work: key scenarios by gap ids or route templates and reuse stored outputs where the deployment hash is unchanged.');
|
|
59
|
+
}
|
|
60
|
+
if (params.budgetWarnings.some((w) => w.includes('truncated') || w.includes('80%'))) {
|
|
61
|
+
rec.push('Tune llmMaxOutputTokensPerCall (or legacy llmTokenBudget) against real prompt sizes, or lower testGenerationLimit so each completion stays within a safe envelope.');
|
|
62
|
+
}
|
|
63
|
+
if (params.scenarioSource === 'template') {
|
|
64
|
+
rec.push('Scenarios were generated from built-in templates (no LLM). This is the lowest-cost path; keep expanding template coverage for recurring gap categories.');
|
|
65
|
+
}
|
|
66
|
+
return rec;
|
|
67
|
+
}
|
|
68
|
+
export function computeDeterministicMaturity(params) {
|
|
69
|
+
if (params.mode === 'auth-required') {
|
|
70
|
+
return {
|
|
71
|
+
level: 0,
|
|
72
|
+
label: 'L0 — unknown / blocked',
|
|
73
|
+
rationale: 'Authentication blocked the crawl. Release confidence and gap inventory are not representative until a deterministic auth path is configured.',
|
|
74
|
+
ceilingNote: 'L1+ require at least one authenticated or public crawl producing structured gaps.',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (params.coveragePagesScanned === 0 && params.gapCount === 0) {
|
|
78
|
+
return {
|
|
79
|
+
level: 0,
|
|
80
|
+
label: 'L0 — insufficient deterministic signal',
|
|
81
|
+
rationale: 'No pages were scanned and no gaps were recorded. Treat maturity as unknown rather than high.',
|
|
82
|
+
ceilingNote: 'Re-run with reachable URLs, auth, or relaxed crawl limits.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let level = 1;
|
|
86
|
+
let label = 'L1 — deterministic scan inventory';
|
|
87
|
+
let rationale = 'Structured gaps came from deterministic crawling and checks (links, console, a11y, coverage). This is the baseline Qulib release-confidence story.';
|
|
88
|
+
if (params.scenarioSource === 'llm') {
|
|
89
|
+
level = 2;
|
|
90
|
+
label = 'L2 — AI-assisted analysis layer';
|
|
91
|
+
rationale =
|
|
92
|
+
'An LLM expanded gaps into scenarios. Value is exploratory; it is not yet the same as enforced CI coverage.';
|
|
93
|
+
}
|
|
94
|
+
if (params.repeatedOperations.length > 0) {
|
|
95
|
+
level = Math.max(level, 3);
|
|
96
|
+
label = 'L3 — repeated AI surface';
|
|
97
|
+
rationale =
|
|
98
|
+
'Repeated prompt fingerprints suggest the same reasoning loop is firing more than once—strong candidates to replace with cached or deterministic checks.';
|
|
99
|
+
}
|
|
100
|
+
const ceilingNote = (params.releaseConfidence ?? 0) >= 85 && !params.requireHumanReview
|
|
101
|
+
? 'High release confidence does not imply L4–L5. Reviewed self-healing (L4) and adaptive quality intelligence (L5) require organizational process and feedback loops outside this single scan.'
|
|
102
|
+
: 'L4 (reviewed self-healing) and L5 (adaptive quality intelligence) are not inferred from a snapshot scan. They require documented review, ownership, and sustained automation feedback.';
|
|
103
|
+
return { level, label, rationale, ceilingNote };
|
|
104
|
+
}
|
|
105
|
+
export function assembleCostIntelligence(params) {
|
|
106
|
+
const usageSummary = summarizeUsageQuality(params.records);
|
|
107
|
+
const budgetWarnings = buildBudgetWarnings(params.records, params.maxOutputTokensPerLlmCall);
|
|
108
|
+
const repeatedOperations = findRepeatedPromptPatterns(params.records);
|
|
109
|
+
const conversionRecommendations = buildConversionRecommendations({
|
|
110
|
+
scenarioSource: params.scenarioSource,
|
|
111
|
+
repeatedOperations,
|
|
112
|
+
budgetWarnings,
|
|
113
|
+
gapCount: params.partial.gaps.length,
|
|
114
|
+
});
|
|
115
|
+
const deterministicMaturity = computeDeterministicMaturity({
|
|
116
|
+
mode: params.partial.mode,
|
|
117
|
+
coveragePagesScanned: params.partial.coveragePagesScanned,
|
|
118
|
+
gapCount: params.partial.gaps.length,
|
|
119
|
+
scenarioSource: params.scenarioSource,
|
|
120
|
+
repeatedOperations,
|
|
121
|
+
releaseConfidence: params.partial.releaseConfidence,
|
|
122
|
+
requireHumanReview: params.requireHumanReview,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
maxOutputTokensPerLlmCall: params.maxOutputTokensPerLlmCall,
|
|
126
|
+
budgetRole: 'max-output-tokens-per-llm-call',
|
|
127
|
+
records: params.records,
|
|
128
|
+
budgetWarnings,
|
|
129
|
+
usageSummary,
|
|
130
|
+
repeatedOperations,
|
|
131
|
+
deterministicMaturity,
|
|
132
|
+
conversionRecommendations,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function costIntelligenceForAuthBlocked(maxOutputTokensPerLlmCall) {
|
|
136
|
+
return {
|
|
137
|
+
maxOutputTokensPerLlmCall,
|
|
138
|
+
budgetRole: 'max-output-tokens-per-llm-call',
|
|
139
|
+
records: [],
|
|
140
|
+
budgetWarnings: [],
|
|
141
|
+
usageSummary: { totalInputTokens: 0, totalOutputTokens: 0, dataQuality: 'none' },
|
|
142
|
+
repeatedOperations: [],
|
|
143
|
+
deterministicMaturity: {
|
|
144
|
+
level: 0,
|
|
145
|
+
label: 'L0 — unknown / blocked',
|
|
146
|
+
rationale: 'Authentication blocked the crawl. No LLM usage was recorded; deterministic inventory is incomplete.',
|
|
147
|
+
ceilingNote: 'Configure auth, then re-run so gap analysis and any LLM scenario pass operate on real pages.',
|
|
148
|
+
},
|
|
149
|
+
conversionRecommendations: [
|
|
150
|
+
'Resolve authentication first (storage-state from `qulib auth init` or form-login flags). Cost intelligence for model-assisted work applies after the crawl succeeds.',
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
package/dist/llm/provider.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { type Gap, type NeutralScenario } from '../schemas/gap-analysis.schema.js';
|
|
2
|
-
export
|
|
2
|
+
export interface LlmCallResult {
|
|
3
|
+
text: string;
|
|
4
|
+
usage: {
|
|
5
|
+
provider: string;
|
|
6
|
+
model: string;
|
|
7
|
+
inputTokens: number;
|
|
8
|
+
outputTokens: number;
|
|
9
|
+
dataQuality: 'actual' | 'estimated';
|
|
10
|
+
} | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function callLLM(prompt: string, tokenBudget: number): Promise<LlmCallResult>;
|
|
3
13
|
export declare function generateScenariosFromTemplate(gaps: Gap[]): NeutralScenario[];
|
|
4
14
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/llm/provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,GAAG,EAAE,KAAK,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAE1G,wBAAsB,OAAO,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/llm/provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,GAAG,EAAE,KAAK,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAE1G,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,QAAQ,GAAG,WAAW,CAAC;KACrC,GAAG,IAAI,CAAC;CACV;AAMD,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAwDzF;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,eAAe,EAAE,CAuC5E"}
|
package/dist/llm/provider.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { NeutralScenarioSchema } from '../schemas/gap-analysis.schema.js';
|
|
3
|
+
function estimateTokensFromChars(chars) {
|
|
4
|
+
return Math.max(0, Math.ceil(chars / 4));
|
|
5
|
+
}
|
|
3
6
|
export async function callLLM(prompt, tokenBudget) {
|
|
4
7
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
5
8
|
if (!apiKey)
|
|
6
9
|
throw new Error('ANTHROPIC_API_KEY is not set');
|
|
10
|
+
const model = 'claude-sonnet-4-20250514';
|
|
7
11
|
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
8
12
|
method: 'POST',
|
|
9
13
|
headers: {
|
|
@@ -12,7 +16,7 @@ export async function callLLM(prompt, tokenBudget) {
|
|
|
12
16
|
'content-type': 'application/json',
|
|
13
17
|
},
|
|
14
18
|
body: JSON.stringify({
|
|
15
|
-
model
|
|
19
|
+
model,
|
|
16
20
|
max_tokens: tokenBudget,
|
|
17
21
|
messages: [{ role: 'user', content: prompt }],
|
|
18
22
|
}),
|
|
@@ -21,9 +25,32 @@ export async function callLLM(prompt, tokenBudget) {
|
|
|
21
25
|
const text = await response.text();
|
|
22
26
|
throw new Error(`LLM call failed: ${response.status} ${text}`);
|
|
23
27
|
}
|
|
24
|
-
const data = await response.json();
|
|
28
|
+
const data = (await response.json());
|
|
25
29
|
const text = data.content.find((b) => b.type === 'text')?.text ?? '';
|
|
26
|
-
|
|
30
|
+
const inTok = data.usage?.input_tokens;
|
|
31
|
+
const outTok = data.usage?.output_tokens;
|
|
32
|
+
if (typeof inTok === 'number' && typeof outTok === 'number') {
|
|
33
|
+
return {
|
|
34
|
+
text,
|
|
35
|
+
usage: {
|
|
36
|
+
provider: 'anthropic',
|
|
37
|
+
model: data.model ?? model,
|
|
38
|
+
inputTokens: inTok,
|
|
39
|
+
outputTokens: outTok,
|
|
40
|
+
dataQuality: 'actual',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
text,
|
|
46
|
+
usage: {
|
|
47
|
+
provider: 'anthropic',
|
|
48
|
+
model: data.model ?? model,
|
|
49
|
+
inputTokens: estimateTokensFromChars(prompt.length),
|
|
50
|
+
outputTokens: estimateTokensFromChars(text.length),
|
|
51
|
+
dataQuality: 'estimated',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
27
54
|
}
|
|
28
55
|
export function generateScenariosFromTemplate(gaps) {
|
|
29
56
|
return gaps.map((gap) => {
|
|
@@ -41,7 +68,19 @@ export function generateScenariosFromTemplate(gaps) {
|
|
|
41
68
|
else if (gap.category === 'broken-link') {
|
|
42
69
|
steps.push({ action: 'assert-visible', description: 'Assert all links resolve correctly' });
|
|
43
70
|
}
|
|
44
|
-
|
|
71
|
+
else if (gap.category === 'auth-surface') {
|
|
72
|
+
steps.push({
|
|
73
|
+
action: 'assert-visible',
|
|
74
|
+
description: 'Verify sign-in surface accessibility and SSO affordances',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else if (gap.category === 'coverage') {
|
|
78
|
+
steps.push({
|
|
79
|
+
action: 'assert-visible',
|
|
80
|
+
description: 'Resolve authentication and re-run full deployment scan',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const adapter = gap.category === 'a11y' || gap.category === 'auth-surface' ? 'accessibility' : 'playwright';
|
|
45
84
|
return NeutralScenarioSchema.parse({
|
|
46
85
|
id: randomUUID(),
|
|
47
86
|
title: `[${gap.severity.toUpperCase()}] ${gap.category} — ${gap.path}`,
|
package/dist/phases/act.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"act.d.ts","sourceRoot":"","sources":["../../src/phases/act.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAIrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,wBAAsB,GAAG,CACvB,QAAQ,EAAE,WAAW,EACrB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,GACxD,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"act.d.ts","sourceRoot":"","sources":["../../src/phases/act.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAIrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,wBAAsB,GAAG,CACvB,QAAQ,EAAE,WAAW,EACrB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,GACxD,OAAO,CAAC,IAAI,CAAC,CA+Cf"}
|
package/dist/phases/act.js
CHANGED
|
@@ -27,7 +27,10 @@ export async function act(analysis, config, artifacts = { writeArtifacts: true }
|
|
|
27
27
|
log('\n[qulib] Analysis complete');
|
|
28
28
|
log(` Gaps found: ${analysis.gaps.length}`);
|
|
29
29
|
log(` Scenarios generated: ${analysis.scenarios.length}`);
|
|
30
|
-
log(` Release confidence: ${analysis.releaseConfidence}/100`);
|
|
30
|
+
log(` Release confidence: ${analysis.releaseConfidence === null ? '— (null)' : `${analysis.releaseConfidence}/100`}`);
|
|
31
|
+
if (analysis.costIntelligence?.budgetWarnings.length) {
|
|
32
|
+
log(` Cost warnings: ${analysis.costIntelligence.budgetWarnings.length} (see report.md Cost Intelligence)`);
|
|
33
|
+
}
|
|
31
34
|
if (config.requireHumanReview) {
|
|
32
35
|
log('\n[qulib] Human review required before applying any generated output.');
|
|
33
36
|
if (artifacts.writeArtifacts) {
|
package/dist/phases/observe.js
CHANGED
|
@@ -8,7 +8,7 @@ export async function observe(baseUrl, repoPath, config, artifacts = { writeArti
|
|
|
8
8
|
const explorer = createExplorer(config.explorer);
|
|
9
9
|
const stateManager = new StateManager();
|
|
10
10
|
const logOpts = { persist: artifacts.writeArtifacts, memory: artifacts.decisionMemory };
|
|
11
|
-
const rawRoutes = await explorer.explore(baseUrl, config);
|
|
11
|
+
const rawRoutes = await explorer.explore(baseUrl, config, artifacts);
|
|
12
12
|
const routes = RouteInventorySchema.parse(rawRoutes);
|
|
13
13
|
if (artifacts.writeArtifacts) {
|
|
14
14
|
await stateManager.writeState('discovered-routes.json', routes, RouteInventorySchema);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type HarnessConfig } from '../schemas/config.schema.js';
|
|
2
|
+
import { type GapAnalysis } from '../schemas/gap-analysis.schema.js';
|
|
3
|
+
import type { RunArtifactsOptions } from '../harness/run-options.js';
|
|
4
|
+
export type GapAnalysisDraft = Omit<GapAnalysis, 'scenarios' | 'generatedTests' | 'costIntelligence'>;
|
|
5
|
+
export declare function finalizeGapAnalysisFromDraft(draft: GapAnalysisDraft, config: HarnessConfig, artifacts?: RunArtifactsOptions, costContext?: Pick<GapAnalysis, 'mode' | 'coveragePagesScanned' | 'releaseConfidence' | 'gaps'>): Promise<GapAnalysis>;
|
|
6
|
+
//# sourceMappingURL=think-finalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"think-finalize.d.ts","sourceRoot":"","sources":["../../src/phases/think-finalize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACnG,OAAO,EAA4C,KAAK,WAAW,EAAwB,MAAM,mCAAmC,CAAC;AAQrI,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,gBAAgB,GAAG,kBAAkB,CAAC,CAAC;AAEtG,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,EACzD,WAAW,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAAC,GAC9F,OAAO,CAAC,WAAW,CAAC,CAwKtB"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { resolveMaxOutputTokensPerLlmCall } from '../schemas/config.schema.js';
|
|
2
|
+
import { GapAnalysisSchema, NeutralScenarioSchema } from '../schemas/gap-analysis.schema.js';
|
|
3
|
+
import { StateManager } from '../harness/state-manager.js';
|
|
4
|
+
import { logDecision } from '../harness/decision-logger.js';
|
|
5
|
+
import { callLLM, generateScenariosFromTemplate } from '../llm/provider.js';
|
|
6
|
+
import { buildGapPrompt } from '../llm/context-builder.js';
|
|
7
|
+
import { assembleCostIntelligence } from '../llm/cost-intelligence.js';
|
|
8
|
+
import { hashForCostIntelligence } from '../llm/content-hash.js';
|
|
9
|
+
export async function finalizeGapAnalysisFromDraft(draft, config, artifacts = { writeArtifacts: true }, costContext) {
|
|
10
|
+
const stateManager = new StateManager();
|
|
11
|
+
const logOpts = { persist: artifacts.writeArtifacts, memory: artifacts.decisionMemory };
|
|
12
|
+
const partialAnalysis = GapAnalysisSchema.parse({
|
|
13
|
+
...draft,
|
|
14
|
+
scenarios: [],
|
|
15
|
+
generatedTests: [],
|
|
16
|
+
});
|
|
17
|
+
let scenarioSource = 'template';
|
|
18
|
+
let generatedScenarios = [];
|
|
19
|
+
const llmRecords = [];
|
|
20
|
+
const maxOut = resolveMaxOutputTokensPerLlmCall(config);
|
|
21
|
+
if (!config.enableLlmScenarios) {
|
|
22
|
+
await logDecision({
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
phase: 'think',
|
|
25
|
+
decision: 'llm-scenarios-disabled',
|
|
26
|
+
reason: 'enableLlmScenarios is false; using template scenarios only',
|
|
27
|
+
}, logOpts);
|
|
28
|
+
generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
|
|
29
|
+
llmRecords.push({
|
|
30
|
+
provider: 'none',
|
|
31
|
+
model: 'none',
|
|
32
|
+
inputTokens: 0,
|
|
33
|
+
outputTokens: 0,
|
|
34
|
+
operationType: 'scenario-generation',
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
dataQuality: 'none',
|
|
37
|
+
notes: 'No LLM call: enableLlmScenarios is false in harness config.',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else if (!process.env.ANTHROPIC_API_KEY) {
|
|
41
|
+
await logDecision({
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
phase: 'think',
|
|
44
|
+
decision: 'llm-scenarios-skipped',
|
|
45
|
+
reason: 'Skipped LLM scenario generation because ANTHROPIC_API_KEY is not set',
|
|
46
|
+
}, logOpts);
|
|
47
|
+
generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
|
|
48
|
+
llmRecords.push({
|
|
49
|
+
provider: 'none',
|
|
50
|
+
model: 'none',
|
|
51
|
+
inputTokens: 0,
|
|
52
|
+
outputTokens: 0,
|
|
53
|
+
operationType: 'scenario-generation',
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
dataQuality: 'none',
|
|
56
|
+
notes: 'No LLM call: ANTHROPIC_API_KEY not set; template scenarios only.',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const prompt = buildGapPrompt(partialAnalysis.gaps, config.testGenerationLimit);
|
|
61
|
+
const promptHash = hashForCostIntelligence(prompt);
|
|
62
|
+
try {
|
|
63
|
+
const llmResult = await callLLM(prompt, maxOut);
|
|
64
|
+
const resultHash = hashForCostIntelligence(llmResult.text);
|
|
65
|
+
const usage = llmResult.usage;
|
|
66
|
+
llmRecords.push({
|
|
67
|
+
provider: usage?.provider ?? 'unknown',
|
|
68
|
+
model: usage?.model ?? 'unknown',
|
|
69
|
+
inputTokens: usage?.inputTokens ?? 0,
|
|
70
|
+
outputTokens: usage?.outputTokens ?? 0,
|
|
71
|
+
operationType: 'scenario-generation',
|
|
72
|
+
timestamp: new Date().toISOString(),
|
|
73
|
+
promptHash,
|
|
74
|
+
resultHash,
|
|
75
|
+
dataQuality: usage?.dataQuality ?? 'estimated',
|
|
76
|
+
notes: usage?.dataQuality === 'estimated'
|
|
77
|
+
? 'Token counts estimated (API usage block missing).'
|
|
78
|
+
: undefined,
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(llmResult.text);
|
|
82
|
+
const candidates = Array.isArray(parsed) ? parsed : [];
|
|
83
|
+
for (const item of candidates) {
|
|
84
|
+
const validated = NeutralScenarioSchema.safeParse(item);
|
|
85
|
+
if (validated.success) {
|
|
86
|
+
generatedScenarios.push(validated.data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (generatedScenarios.length === 0) {
|
|
90
|
+
generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
|
|
91
|
+
scenarioSource = 'template';
|
|
92
|
+
const last = llmRecords[llmRecords.length - 1];
|
|
93
|
+
if (last) {
|
|
94
|
+
last.notes = [last.notes, 'Model returned no valid scenarios; template scenarios used.']
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(' ');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
scenarioSource = 'llm';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
|
|
105
|
+
scenarioSource = 'template';
|
|
106
|
+
const last = llmRecords[llmRecords.length - 1];
|
|
107
|
+
if (last) {
|
|
108
|
+
last.notes = [last.notes, 'Response was not valid JSON; template scenarios used.']
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join(' ');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
|
|
116
|
+
scenarioSource = 'template';
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
llmRecords.push({
|
|
119
|
+
provider: 'unavailable',
|
|
120
|
+
model: 'unknown',
|
|
121
|
+
inputTokens: Math.max(0, Math.ceil(prompt.length / 4)),
|
|
122
|
+
outputTokens: 0,
|
|
123
|
+
operationType: 'scenario-generation',
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
promptHash,
|
|
126
|
+
dataQuality: 'estimated',
|
|
127
|
+
notes: `LLM call failed; template scenarios used. ${msg.slice(0, 240)}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await logDecision({
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
phase: 'think',
|
|
134
|
+
decision: 'scenario-generation-complete',
|
|
135
|
+
reason: `Generated ${generatedScenarios.length} scenarios via ${scenarioSource}`,
|
|
136
|
+
metadata: {
|
|
137
|
+
source: scenarioSource,
|
|
138
|
+
scenarioCount: generatedScenarios.length,
|
|
139
|
+
},
|
|
140
|
+
}, logOpts);
|
|
141
|
+
const costPartial = costContext ?? partialAnalysis;
|
|
142
|
+
const costIntelligence = assembleCostIntelligence({
|
|
143
|
+
maxOutputTokensPerLlmCall: maxOut,
|
|
144
|
+
records: llmRecords,
|
|
145
|
+
partial: {
|
|
146
|
+
mode: costPartial.mode,
|
|
147
|
+
coveragePagesScanned: costPartial.coveragePagesScanned,
|
|
148
|
+
releaseConfidence: costPartial.releaseConfidence ?? 0,
|
|
149
|
+
gaps: costPartial.gaps,
|
|
150
|
+
},
|
|
151
|
+
scenarioSource,
|
|
152
|
+
requireHumanReview: config.requireHumanReview,
|
|
153
|
+
});
|
|
154
|
+
const completeAnalysis = GapAnalysisSchema.parse({
|
|
155
|
+
...partialAnalysis,
|
|
156
|
+
scenarios: generatedScenarios,
|
|
157
|
+
generatedTests: [],
|
|
158
|
+
costIntelligence,
|
|
159
|
+
});
|
|
160
|
+
if (artifacts.writeArtifacts) {
|
|
161
|
+
await stateManager.writeState('gap-analysis.json', completeAnalysis, GapAnalysisSchema);
|
|
162
|
+
}
|
|
163
|
+
return completeAnalysis;
|
|
164
|
+
}
|
package/dist/phases/think.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ import { type GapAnalysis } from '../schemas/gap-analysis.schema.js';
|
|
|
3
3
|
import type { ObserveResult } from './observe.js';
|
|
4
4
|
import type { RunArtifactsOptions } from '../harness/run-options.js';
|
|
5
5
|
export declare function think(observed: ObserveResult, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<GapAnalysis>;
|
|
6
|
+
export { finalizeGapAnalysisFromDraft } from './think-finalize.js';
|
|
7
|
+
export type { GapAnalysisDraft } from './think-finalize.js';
|
|
6
8
|
//# sourceMappingURL=think.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"think.d.ts","sourceRoot":"","sources":["../../src/phases/think.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,
|
|
1
|
+
{"version":3,"file":"think.d.ts","sourceRoot":"","sources":["../../src/phases/think.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAGrE,wBAAsB,KAAK,CACzB,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,GACxD,OAAO,CAAC,WAAW,CAAC,CAqCtB;AAED,OAAO,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACnE,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/phases/think.js
CHANGED
|
@@ -1,85 +1,36 @@
|
|
|
1
|
-
import { GapAnalysisSchema
|
|
1
|
+
import { GapAnalysisSchema } from '../schemas/gap-analysis.schema.js';
|
|
2
2
|
import { analyzeGaps } from '../tools/gap-engine.js';
|
|
3
|
-
import { StateManager } from '../harness/state-manager.js';
|
|
4
3
|
import { logDecision } from '../harness/decision-logger.js';
|
|
5
|
-
import {
|
|
6
|
-
import { buildGapPrompt } from '../llm/context-builder.js';
|
|
4
|
+
import { finalizeGapAnalysisFromDraft } from './think-finalize.js';
|
|
7
5
|
export async function think(observed, config, artifacts = { writeArtifacts: true }) {
|
|
8
6
|
const mode = observed.repo ? 'url-repo' : 'url-only';
|
|
9
|
-
const stateManager = new StateManager();
|
|
10
7
|
const logOpts = { persist: artifacts.writeArtifacts, memory: artifacts.decisionMemory };
|
|
8
|
+
const gapBlock = analyzeGaps(observed.routes, observed.repo, mode, config);
|
|
9
|
+
const draft = {
|
|
10
|
+
analyzedAt: gapBlock.analyzedAt,
|
|
11
|
+
mode: gapBlock.mode,
|
|
12
|
+
releaseConfidence: gapBlock.releaseConfidence,
|
|
13
|
+
coveragePagesScanned: gapBlock.coveragePagesScanned,
|
|
14
|
+
coverageBudgetExceeded: gapBlock.coverageBudgetExceeded,
|
|
15
|
+
coverageWarning: gapBlock.coverageWarning,
|
|
16
|
+
gaps: gapBlock.gaps,
|
|
17
|
+
};
|
|
11
18
|
const partialAnalysis = GapAnalysisSchema.parse({
|
|
12
|
-
...
|
|
19
|
+
...draft,
|
|
13
20
|
scenarios: [],
|
|
14
21
|
generatedTests: [],
|
|
15
22
|
});
|
|
16
|
-
if (artifacts.writeArtifacts) {
|
|
17
|
-
await stateManager.writeState('gap-analysis.json', partialAnalysis, GapAnalysisSchema);
|
|
18
|
-
}
|
|
19
23
|
await logDecision({
|
|
20
24
|
timestamp: new Date().toISOString(),
|
|
21
25
|
phase: 'think',
|
|
22
26
|
decision: 'gap-analysis-complete',
|
|
23
|
-
reason: `Computed ${partialAnalysis.gaps.length} gaps with release confidence ${partialAnalysis.releaseConfidence}`,
|
|
27
|
+
reason: `Computed ${partialAnalysis.gaps.length} gaps with release confidence ${String(partialAnalysis.releaseConfidence)}`,
|
|
24
28
|
metadata: {
|
|
25
29
|
mode,
|
|
26
30
|
gapCount: partialAnalysis.gaps.length,
|
|
27
31
|
releaseConfidence: partialAnalysis.releaseConfidence,
|
|
28
32
|
},
|
|
29
33
|
}, logOpts);
|
|
30
|
-
|
|
31
|
-
let generatedScenarios = [];
|
|
32
|
-
if (!process.env.ANTHROPIC_API_KEY) {
|
|
33
|
-
await logDecision({
|
|
34
|
-
timestamp: new Date().toISOString(),
|
|
35
|
-
phase: 'think',
|
|
36
|
-
decision: 'llm-scenarios-skipped',
|
|
37
|
-
reason: 'Skipped LLM scenario generation because ANTHROPIC_API_KEY is not set',
|
|
38
|
-
}, logOpts);
|
|
39
|
-
generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
try {
|
|
43
|
-
const prompt = buildGapPrompt(partialAnalysis.gaps, config.testGenerationLimit);
|
|
44
|
-
const response = await callLLM(prompt, config.llmTokenBudget);
|
|
45
|
-
const parsed = JSON.parse(response);
|
|
46
|
-
const candidates = Array.isArray(parsed) ? parsed : [];
|
|
47
|
-
for (const item of candidates) {
|
|
48
|
-
const validated = NeutralScenarioSchema.safeParse(item);
|
|
49
|
-
if (validated.success) {
|
|
50
|
-
generatedScenarios.push(validated.data);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (generatedScenarios.length === 0) {
|
|
54
|
-
generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
|
|
55
|
-
scenarioSource = 'template';
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
scenarioSource = 'llm';
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
|
|
63
|
-
scenarioSource = 'template';
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
await logDecision({
|
|
67
|
-
timestamp: new Date().toISOString(),
|
|
68
|
-
phase: 'think',
|
|
69
|
-
decision: 'scenario-generation-complete',
|
|
70
|
-
reason: `Generated ${generatedScenarios.length} scenarios via ${scenarioSource}`,
|
|
71
|
-
metadata: {
|
|
72
|
-
source: scenarioSource,
|
|
73
|
-
scenarioCount: generatedScenarios.length,
|
|
74
|
-
},
|
|
75
|
-
}, logOpts);
|
|
76
|
-
const completeAnalysis = GapAnalysisSchema.parse({
|
|
77
|
-
...partialAnalysis,
|
|
78
|
-
scenarios: generatedScenarios,
|
|
79
|
-
generatedTests: [],
|
|
80
|
-
});
|
|
81
|
-
if (artifacts.writeArtifacts) {
|
|
82
|
-
await stateManager.writeState('gap-analysis.json', completeAnalysis, GapAnalysisSchema);
|
|
83
|
-
}
|
|
84
|
-
return completeAnalysis;
|
|
34
|
+
return finalizeGapAnalysisFromDraft(draft, config, artifacts);
|
|
85
35
|
}
|
|
36
|
+
export { finalizeGapAnalysisFromDraft } from './think-finalize.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-reporter.d.ts","sourceRoot":"","sources":["../../src/reporters/markdown-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAErE,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"markdown-reporter.d.ts","sourceRoot":"","sources":["../../src/reporters/markdown-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAErE,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkEjG"}
|
|
@@ -2,19 +2,38 @@ import { writeFile, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
export async function writeMarkdownReport(analysis, outputDir) {
|
|
4
4
|
await mkdir(outputDir, { recursive: true });
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const rc = analysis.releaseConfidence;
|
|
6
|
+
const recommendation = rc === null
|
|
7
|
+
? 'NOT SCORED (blocked or insufficient surface)'
|
|
8
|
+
: rc >= 80
|
|
9
|
+
? 'READY'
|
|
10
|
+
: rc >= 50
|
|
11
|
+
? 'CONDITIONAL'
|
|
12
|
+
: 'NOT READY';
|
|
7
13
|
const gapRows = analysis.gaps
|
|
8
14
|
.map((g) => `| ${g.path} | ${g.category} | ${g.severity} | ${g.reason} |`)
|
|
9
15
|
.join('\n');
|
|
10
16
|
const scenarioBlocks = analysis.scenarios
|
|
11
17
|
.map((s) => `### ${s.title}\n${s.description}\n\nSteps:\n${s.steps.map((step) => `- ${step.description}`).join('\n')}\n\nRecommended adapters: ${s.recommendations.map((r) => r.adapter).join(', ')}`)
|
|
12
18
|
.join('\n\n---\n\n');
|
|
19
|
+
const ci = analysis.costIntelligence;
|
|
20
|
+
const costSection = ci
|
|
21
|
+
? `## Cost Intelligence
|
|
22
|
+
|
|
23
|
+
- **Per-completion LLM output ceiling:** ${ci.maxOutputTokensPerLlmCall} (${ci.budgetRole.replace(/-/g, ' ')})
|
|
24
|
+
- **Meaning:** this caps **one** model completion per scenario-generation call; it is **not** a multi-step or multi-run token budget.
|
|
25
|
+
- **Usage (this run):** input ${ci.usageSummary.totalInputTokens}, output ${ci.usageSummary.totalOutputTokens} tokens — _${ci.usageSummary.dataQuality}_
|
|
26
|
+
- **Budget warnings:** ${ci.budgetWarnings.length ? ci.budgetWarnings.map((w) => `\n - ${w}`).join('') : '_none_'}
|
|
27
|
+
- **Repeated AI patterns:** ${ci.repeatedOperations.length ? ci.repeatedOperations.map((r) => `\n - \`${r.promptHash}\` ×${r.count}: ${r.recommendation}`).join('') : '_none detected in this run_'}
|
|
28
|
+
- **Deterministic maturity:** **${ci.deterministicMaturity.label}** (level ${ci.deterministicMaturity.level}/5) — ${ci.deterministicMaturity.rationale}${ci.deterministicMaturity.ceilingNote ? `\n - _${ci.deterministicMaturity.ceilingNote}_` : ''}
|
|
29
|
+
- **Conversion recommendations:**${ci.conversionRecommendations.length ? ci.conversionRecommendations.map((c) => `\n - ${c}`).join('') : '\n - _none_'}
|
|
30
|
+
`
|
|
31
|
+
: '';
|
|
13
32
|
const md = `# Qulib Quality Gap Report
|
|
14
33
|
|
|
15
34
|
**Generated:** ${analysis.analyzedAt}
|
|
16
35
|
**Mode:** ${analysis.mode}
|
|
17
|
-
**Release confidence:** ${
|
|
36
|
+
**Release confidence:** ${rc === null ? '—' : `${rc}/100`} — ${recommendation}
|
|
18
37
|
|
|
19
38
|
## Coverage
|
|
20
39
|
|
|
@@ -22,6 +41,7 @@ export async function writeMarkdownReport(analysis, outputDir) {
|
|
|
22
41
|
- Scan budget exhausted (unfinished queue): ${analysis.coverageBudgetExceeded ? 'yes' : 'no'}
|
|
23
42
|
${analysis.coverageWarning ? `- Warning: **${analysis.coverageWarning}**` : '- Warning: none'}
|
|
24
43
|
|
|
44
|
+
${costSection}
|
|
25
45
|
## Coverage gaps (${analysis.gaps.length})
|
|
26
46
|
|
|
27
47
|
| Path | Category | Severity | Reason |
|