@qulib/core 0.8.2 → 0.9.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 +30 -5
- package/bin/qulib.js +2 -3
- package/dist/__tests__/playwright-available.d.ts +32 -0
- package/dist/__tests__/playwright-available.d.ts.map +1 -0
- package/dist/__tests__/playwright-available.js +35 -0
- package/dist/adapters/ci-results-adapter.d.ts +67 -0
- package/dist/adapters/ci-results-adapter.d.ts.map +1 -0
- package/dist/adapters/ci-results-adapter.js +143 -0
- package/dist/adapters/cypress-e2e-adapter.d.ts.map +1 -1
- package/dist/adapters/cypress-e2e-adapter.js +25 -2
- package/dist/adapters/playwright-adapter.d.ts.map +1 -1
- package/dist/adapters/playwright-adapter.js +25 -2
- package/dist/adapters/pr-metadata-adapter.d.ts +75 -0
- package/dist/adapters/pr-metadata-adapter.d.ts.map +1 -0
- package/dist/adapters/pr-metadata-adapter.js +146 -0
- package/dist/adapters/validate-specs.d.ts +55 -0
- package/dist/adapters/validate-specs.d.ts.map +1 -0
- package/dist/adapters/validate-specs.js +67 -0
- package/dist/baseline/baseline.d.ts +54 -0
- package/dist/baseline/baseline.d.ts.map +1 -0
- package/dist/baseline/baseline.js +252 -0
- package/dist/baseline/baseline.schema.d.ts +233 -0
- package/dist/baseline/baseline.schema.d.ts.map +1 -0
- package/dist/baseline/baseline.schema.js +59 -0
- package/dist/cli/confidence-run.d.ts +16 -0
- package/dist/cli/confidence-run.d.ts.map +1 -0
- package/dist/cli/confidence-run.js +158 -0
- package/dist/cli/index.d.ts +11 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +80 -4
- package/dist/cli/scaffold-run.d.ts +86 -0
- package/dist/cli/scaffold-run.d.ts.map +1 -0
- package/dist/cli/scaffold-run.js +232 -0
- package/dist/cli/score-automation-run.d.ts +25 -0
- package/dist/cli/score-automation-run.d.ts.map +1 -0
- package/dist/cli/score-automation-run.js +123 -0
- package/dist/examples/notquality-dogfood/fixture.d.ts +166 -0
- package/dist/examples/notquality-dogfood/fixture.d.ts.map +1 -0
- package/dist/examples/notquality-dogfood/fixture.js +174 -0
- package/dist/examples/notquality-dogfood/run.d.ts +34 -0
- package/dist/examples/notquality-dogfood/run.d.ts.map +1 -0
- package/dist/examples/notquality-dogfood/run.js +139 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/recipes/a11y.d.ts +36 -0
- package/dist/recipes/a11y.d.ts.map +1 -0
- package/dist/recipes/a11y.js +118 -0
- package/dist/recipes/auth.d.ts +38 -0
- package/dist/recipes/auth.d.ts.map +1 -0
- package/dist/recipes/auth.js +156 -0
- package/dist/recipes/index.d.ts +26 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +41 -0
- package/dist/recipes/nav.d.ts +34 -0
- package/dist/recipes/nav.d.ts.map +1 -0
- package/dist/recipes/nav.js +128 -0
- package/dist/recipes/seed.d.ts +34 -0
- package/dist/recipes/seed.d.ts.map +1 -0
- package/dist/recipes/seed.js +87 -0
- package/dist/scaffold-tests.d.ts +21 -0
- package/dist/scaffold-tests.d.ts.map +1 -1
- package/dist/scaffold-tests.js +12 -2
- package/dist/schemas/confidence.schema.d.ts +526 -0
- package/dist/schemas/confidence.schema.d.ts.map +1 -0
- package/dist/schemas/confidence.schema.js +161 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +3 -0
- package/dist/schemas/recipe.schema.d.ts +66 -0
- package/dist/schemas/recipe.schema.d.ts.map +1 -0
- package/dist/schemas/recipe.schema.js +45 -0
- package/dist/schemas/views.schema.d.ts +234 -0
- package/dist/schemas/views.schema.d.ts.map +1 -0
- package/dist/schemas/views.schema.js +82 -0
- package/dist/tools/scoring/confidence-from-qulib.d.ts +34 -0
- package/dist/tools/scoring/confidence-from-qulib.d.ts.map +1 -0
- package/dist/tools/scoring/confidence-from-qulib.js +206 -0
- package/dist/tools/scoring/confidence-views.d.ts +40 -0
- package/dist/tools/scoring/confidence-views.d.ts.map +1 -0
- package/dist/tools/scoring/confidence-views.js +163 -0
- package/dist/tools/scoring/confidence.d.ts +32 -0
- package/dist/tools/scoring/confidence.d.ts.map +1 -0
- package/dist/tools/scoring/confidence.js +180 -0
- package/dist/tools/scoring/levels.d.ts +15 -0
- package/dist/tools/scoring/levels.d.ts.map +1 -0
- package/dist/tools/scoring/levels.js +21 -0
- package/package.json +13 -7
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `qulib scaffold` — Q2 (scaffold-cli subtask).
|
|
3
|
+
*
|
|
4
|
+
* Wraps `scaffoldTests(url, options)` from ../scaffold-tests.js as a first-class
|
|
5
|
+
* CLI surface (scaffold was previously only reachable programmatically / via MCP).
|
|
6
|
+
*
|
|
7
|
+
* This file owns the `scaffold` subcommand end-to-end and is registered from
|
|
8
|
+
* cli/index.ts via `registerScaffoldCommand(program)` so this build agent never
|
|
9
|
+
* edits index.ts itself (avoids collision with score-automation). It mirrors the
|
|
10
|
+
* dynamic-import command style used by `cost` and the output-mode conventions of
|
|
11
|
+
* `analyze` (write-to-disk by default, `--json` for a stdout-only run).
|
|
12
|
+
*
|
|
13
|
+
* Output modes (one stdout shape, mutually exclusive with disk writes):
|
|
14
|
+
* default → write projectConfig + generated specs under --out (./qulib-scaffold)
|
|
15
|
+
* --json → no disk writes; print the full ScaffoldResult-shaped JSON on stdout
|
|
16
|
+
*
|
|
17
|
+
* Honesty rules (root design principle: never emit false confidence):
|
|
18
|
+
* - If analyze produced ZERO scenarios, we do NOT write an empty-but-confident
|
|
19
|
+
* scaffold. Write mode exits non-zero with a clear message; --json emits an
|
|
20
|
+
* explicit `{ empty: true, ... }` payload so a caller/agent can branch on it.
|
|
21
|
+
* - `--framework playwright` currently maps to a not-implemented adapter
|
|
22
|
+
* (PlaywrightAdapter.renderAll throws). Rather than surfacing a raw stack, we
|
|
23
|
+
* translate it into an actionable error pointing at the supported framework.
|
|
24
|
+
*/
|
|
25
|
+
import type { Command } from 'commander';
|
|
26
|
+
import { type ScaffoldResult } from '../scaffold-tests.js';
|
|
27
|
+
import type { SpecValidationReport } from '../adapters/validate-specs.js';
|
|
28
|
+
import { type RecipeId } from '../schemas/recipe.schema.js';
|
|
29
|
+
/** Frameworks `scaffoldTests` accepts. Mirrors its `ScaffoldOptions['framework']`. */
|
|
30
|
+
declare const FRAMEWORKS: readonly ["cypress-e2e", "playwright"];
|
|
31
|
+
type ScaffoldFramework = (typeof FRAMEWORKS)[number];
|
|
32
|
+
/** A single file the scaffold wants on disk, with its repo-relative path. */
|
|
33
|
+
interface ScaffoldFile {
|
|
34
|
+
/** Path relative to the --out root. */
|
|
35
|
+
relativePath: string;
|
|
36
|
+
contents: string;
|
|
37
|
+
}
|
|
38
|
+
interface ScaffoldRunOptions {
|
|
39
|
+
url: string;
|
|
40
|
+
framework: ScaffoldFramework;
|
|
41
|
+
maxPages?: number;
|
|
42
|
+
out: string;
|
|
43
|
+
json: boolean;
|
|
44
|
+
recipes?: RecipeId[];
|
|
45
|
+
/**
|
|
46
|
+
* When true, fail the command (non-zero exit) if any generated spec does not
|
|
47
|
+
* parse/compile. The dry-run validation always runs; this flag controls
|
|
48
|
+
* whether a validation failure is fatal vs merely reported.
|
|
49
|
+
*/
|
|
50
|
+
validateSpecs?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Raised when `--validate-specs` is set and at least one generated spec fails
|
|
54
|
+
* the dry-run. Carries a non-zero `exitCode` so the CLI surfaces a hard failure
|
|
55
|
+
* instead of writing a known-broken scaffold and exiting green.
|
|
56
|
+
*/
|
|
57
|
+
export declare class SpecValidationError extends Error {
|
|
58
|
+
readonly exitCode = 1;
|
|
59
|
+
constructor(message: string);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Flatten a ScaffoldResult into the concrete files a scaffold project needs:
|
|
63
|
+
* the framework config file, any support files, and one spec per generated test
|
|
64
|
+
* (each at its own `outputPath`, which the adapter already namespaces e.g.
|
|
65
|
+
* `cypress/e2e/<slug>.cy.ts`). Pure + side-effect-free so tests can assert on it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function collectScaffoldFiles(result: ScaffoldResult): ScaffoldFile[];
|
|
68
|
+
/**
|
|
69
|
+
* Apply the dry-run validation gate.
|
|
70
|
+
*
|
|
71
|
+
* Pure + side-effect-free (returns the warning text instead of logging) so it
|
|
72
|
+
* is unit-testable in isolation: feed it an `ok: false` report and assert it
|
|
73
|
+
* throws; feed it an `ok: true` report and assert it returns null. This is the
|
|
74
|
+
* discrimination witness for the fatal path — `runScaffold` cannot produce a
|
|
75
|
+
* broken spec on demand (the adapters always render valid code), so the gate's
|
|
76
|
+
* reject-vs-pass behavior is proven here directly against a report.
|
|
77
|
+
*
|
|
78
|
+
* @returns a non-null warning string when validation failed but `validateSpecs`
|
|
79
|
+
* was not set (caller should log it); null when all specs parsed.
|
|
80
|
+
* @throws SpecValidationError when validation failed AND `validateSpecs` is set.
|
|
81
|
+
*/
|
|
82
|
+
export declare function enforceSpecValidation(validation: SpecValidationReport, validateSpecs: boolean): string | null;
|
|
83
|
+
export declare function runScaffold(options: ScaffoldRunOptions): Promise<void>;
|
|
84
|
+
export declare function registerScaffoldCommand(program: Command): void;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=scaffold-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold-run.d.ts","sourceRoot":"","sources":["../../src/cli/scaffold-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAI5E,sFAAsF;AACtF,QAAA,MAAM,UAAU,wCAAyC,CAAC;AAC1D,KAAK,iBAAiB,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAKrD,6EAA6E;AAC7E,UAAU,YAAY;IACpB,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,kBAAkB;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,iBAAiB,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,QAAQ,KAAK;gBACV,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,EAAE,CAgC3E;AAOD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,oBAAoB,EAChC,aAAa,EAAE,OAAO,GACrB,MAAM,GAAG,IAAI,CAef;AA+BD,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiG5E;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkE9D"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { scaffoldTests } from '../scaffold-tests.js';
|
|
3
|
+
import { RecipeIdSchema } from '../schemas/recipe.schema.js';
|
|
4
|
+
const ScaffoldUrlSchema = z.string().url();
|
|
5
|
+
/** Frameworks `scaffoldTests` accepts. Mirrors its `ScaffoldOptions['framework']`. */
|
|
6
|
+
const FRAMEWORKS = ['cypress-e2e', 'playwright'];
|
|
7
|
+
const FrameworkSchema = z.enum(FRAMEWORKS);
|
|
8
|
+
const DEFAULT_OUT_DIR = 'qulib-scaffold';
|
|
9
|
+
/**
|
|
10
|
+
* Raised when `--validate-specs` is set and at least one generated spec fails
|
|
11
|
+
* the dry-run. Carries a non-zero `exitCode` so the CLI surfaces a hard failure
|
|
12
|
+
* instead of writing a known-broken scaffold and exiting green.
|
|
13
|
+
*/
|
|
14
|
+
export class SpecValidationError extends Error {
|
|
15
|
+
exitCode = 1;
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'SpecValidationError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Flatten a ScaffoldResult into the concrete files a scaffold project needs:
|
|
23
|
+
* the framework config file, any support files, and one spec per generated test
|
|
24
|
+
* (each at its own `outputPath`, which the adapter already namespaces e.g.
|
|
25
|
+
* `cypress/e2e/<slug>.cy.ts`). Pure + side-effect-free so tests can assert on it.
|
|
26
|
+
*/
|
|
27
|
+
export function collectScaffoldFiles(result) {
|
|
28
|
+
const files = [];
|
|
29
|
+
const { projectConfig, generatedTests } = result;
|
|
30
|
+
files.push({
|
|
31
|
+
relativePath: projectConfig.configFile.filename,
|
|
32
|
+
contents: projectConfig.configFile.code,
|
|
33
|
+
});
|
|
34
|
+
for (const support of projectConfig.supportFiles) {
|
|
35
|
+
files.push({ relativePath: support.filename, contents: support.code });
|
|
36
|
+
}
|
|
37
|
+
for (const spec of generatedTests) {
|
|
38
|
+
files.push({ relativePath: spec.outputPath, contents: spec.code });
|
|
39
|
+
}
|
|
40
|
+
// A package.json fragment is informational (devDeps + scripts) — surface it as
|
|
41
|
+
// a file so the scaffold is runnable without the caller re-deriving it.
|
|
42
|
+
files.push({
|
|
43
|
+
relativePath: 'package.json',
|
|
44
|
+
contents: JSON.stringify({
|
|
45
|
+
name: 'qulib-scaffolded-suite',
|
|
46
|
+
private: true,
|
|
47
|
+
scripts: projectConfig.packageJson.scripts,
|
|
48
|
+
devDependencies: projectConfig.packageJson.devDependencies,
|
|
49
|
+
}, null, 2) + '\n',
|
|
50
|
+
});
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
/** True when scaffold produced nothing actionable (no scenarios → no specs). */
|
|
54
|
+
function isEmptyScaffold(result) {
|
|
55
|
+
return result.scenarios.length === 0 || result.generatedTests.length === 0;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Apply the dry-run validation gate.
|
|
59
|
+
*
|
|
60
|
+
* Pure + side-effect-free (returns the warning text instead of logging) so it
|
|
61
|
+
* is unit-testable in isolation: feed it an `ok: false` report and assert it
|
|
62
|
+
* throws; feed it an `ok: true` report and assert it returns null. This is the
|
|
63
|
+
* discrimination witness for the fatal path — `runScaffold` cannot produce a
|
|
64
|
+
* broken spec on demand (the adapters always render valid code), so the gate's
|
|
65
|
+
* reject-vs-pass behavior is proven here directly against a report.
|
|
66
|
+
*
|
|
67
|
+
* @returns a non-null warning string when validation failed but `validateSpecs`
|
|
68
|
+
* was not set (caller should log it); null when all specs parsed.
|
|
69
|
+
* @throws SpecValidationError when validation failed AND `validateSpecs` is set.
|
|
70
|
+
*/
|
|
71
|
+
export function enforceSpecValidation(validation, validateSpecs) {
|
|
72
|
+
if (validation.ok)
|
|
73
|
+
return null;
|
|
74
|
+
const failed = validation.results.filter((r) => !r.valid);
|
|
75
|
+
const detail = failed
|
|
76
|
+
.map((r) => ` ✗ ${r.outputPath}\n ${r.errors.join('\n ')}`)
|
|
77
|
+
.join('\n');
|
|
78
|
+
const summary = `${validation.invalidCount} of ${validation.total} generated spec(s) failed dry-run validation ` +
|
|
79
|
+
`(they do not parse/compile):\n${detail}`;
|
|
80
|
+
if (validateSpecs) {
|
|
81
|
+
throw new SpecValidationError(summary);
|
|
82
|
+
}
|
|
83
|
+
return summary;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Translate the known not-implemented-adapter failure into an actionable message.
|
|
87
|
+
* Re-throws anything else unchanged so real failures stay loud.
|
|
88
|
+
*/
|
|
89
|
+
function rethrowScaffoldError(error, framework) {
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
+
if (/not implemented/i.test(message)) {
|
|
92
|
+
throw new Error(`The "${framework}" test adapter is not implemented yet, so qulib cannot render specs for it. ` +
|
|
93
|
+
`Re-run with --framework cypress-e2e (the supported scaffolder) until the ${framework} adapter ships.`);
|
|
94
|
+
}
|
|
95
|
+
throw error instanceof Error ? error : new Error(message);
|
|
96
|
+
}
|
|
97
|
+
async function writeScaffoldToDisk(files, outDir) {
|
|
98
|
+
const fs = await import('node:fs/promises');
|
|
99
|
+
const path = await import('node:path');
|
|
100
|
+
const outRoot = path.resolve(process.cwd(), outDir);
|
|
101
|
+
const written = [];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const dest = path.join(outRoot, file.relativePath);
|
|
104
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
105
|
+
await fs.writeFile(dest, file.contents, 'utf8');
|
|
106
|
+
written.push(dest);
|
|
107
|
+
}
|
|
108
|
+
return written;
|
|
109
|
+
}
|
|
110
|
+
export async function runScaffold(options) {
|
|
111
|
+
const url = ScaffoldUrlSchema.parse(options.url);
|
|
112
|
+
const framework = FrameworkSchema.parse(options.framework);
|
|
113
|
+
if (options.json) {
|
|
114
|
+
console.error('[qulib] Scaffold JSON mode: no disk writes; full result JSON on stdout');
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.error(`[qulib] Scaffolding ${framework} tests for ${url}`);
|
|
118
|
+
console.error('[qulib] Analyzing the deployed surface to derive scenarios — this may take a moment...');
|
|
119
|
+
}
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
result = await scaffoldTests(url, {
|
|
123
|
+
framework,
|
|
124
|
+
...(options.maxPages !== undefined && { maxPagesToScan: options.maxPages }),
|
|
125
|
+
...(options.recipes && options.recipes.length > 0 && { recipes: options.recipes }),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
rethrowScaffoldError(error, framework);
|
|
130
|
+
}
|
|
131
|
+
// Honest empty handling — no false-confidence scaffolds.
|
|
132
|
+
if (isEmptyScaffold(result)) {
|
|
133
|
+
if (options.json) {
|
|
134
|
+
console.log(JSON.stringify({
|
|
135
|
+
url: result.url,
|
|
136
|
+
framework: result.framework,
|
|
137
|
+
empty: true,
|
|
138
|
+
scenarioCount: 0,
|
|
139
|
+
generatedTestCount: 0,
|
|
140
|
+
note: 'Analysis surfaced no scenarios for this URL, so no tests were generated. ' +
|
|
141
|
+
'This is honest output, not a failure: there was nothing concrete to scaffold. ' +
|
|
142
|
+
'Try a different URL, raise --max-pages, or provide authenticated access if the app is behind a login.',
|
|
143
|
+
}, null, 2));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
throw new Error('No scenarios were derived for this URL, so qulib generated no tests. ' +
|
|
147
|
+
'Refusing to write an empty scaffold (no false confidence). ' +
|
|
148
|
+
'Try a different URL, raise --max-pages, or supply auth if the app is behind a login.');
|
|
149
|
+
}
|
|
150
|
+
// Dry-run gate: the scaffold already validated every generated spec through
|
|
151
|
+
// the TS compiler. Surface a failure here so a broken generator output never
|
|
152
|
+
// silently lands on disk. With --validate-specs this is fatal (non-zero exit
|
|
153
|
+
// via SpecValidationError); without it we still warn so the signal is never
|
|
154
|
+
// hidden. The gate logic lives in enforceSpecValidation (unit-tested).
|
|
155
|
+
const validation = result.specValidation;
|
|
156
|
+
const warning = enforceSpecValidation(validation, Boolean(options.validateSpecs));
|
|
157
|
+
if (warning) {
|
|
158
|
+
console.error(`[qulib] WARNING — ${warning}`);
|
|
159
|
+
console.error('[qulib] Re-run with --validate-specs to make this a hard (non-zero) failure.');
|
|
160
|
+
}
|
|
161
|
+
if (options.json) {
|
|
162
|
+
console.log(JSON.stringify({
|
|
163
|
+
url: result.url,
|
|
164
|
+
framework: result.framework,
|
|
165
|
+
empty: false,
|
|
166
|
+
scenarioCount: result.scenarios.length,
|
|
167
|
+
generatedTestCount: result.generatedTests.length,
|
|
168
|
+
scenarios: result.scenarios,
|
|
169
|
+
generatedTests: result.generatedTests,
|
|
170
|
+
projectConfig: result.projectConfig,
|
|
171
|
+
specValidation: result.specValidation,
|
|
172
|
+
}, null, 2));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const files = collectScaffoldFiles(result);
|
|
176
|
+
const written = await writeScaffoldToDisk(files, options.out);
|
|
177
|
+
const path = await import('node:path');
|
|
178
|
+
const outRoot = path.resolve(process.cwd(), options.out);
|
|
179
|
+
console.error(`\n[qulib] Scaffold complete — ${framework}`);
|
|
180
|
+
console.error(` Scenarios derived: ${result.scenarios.length}`);
|
|
181
|
+
console.error(` Specs generated: ${result.generatedTests.length}`);
|
|
182
|
+
console.error(` Specs validated: ${validation.total - validation.invalidCount}/${validation.total} parse cleanly`);
|
|
183
|
+
console.error(` Files written: ${written.length}`);
|
|
184
|
+
console.error(` Output directory: ${outRoot}`);
|
|
185
|
+
console.error(` Config: ${result.projectConfig.configFile.filename}`);
|
|
186
|
+
console.error('\n[qulib] Next: cd into the output dir, `npm install`, then run the test script.');
|
|
187
|
+
}
|
|
188
|
+
export function registerScaffoldCommand(program) {
|
|
189
|
+
program
|
|
190
|
+
.command('scaffold')
|
|
191
|
+
.description('Generate a runnable test suite (config + specs) for a deployed app by analyzing its surface')
|
|
192
|
+
.requiredOption('--url <url>', 'Base URL of the app to scaffold tests for')
|
|
193
|
+
.option('--framework <framework>', `Test framework: ${FRAMEWORKS.join(' | ')}`, 'cypress-e2e')
|
|
194
|
+
.option('--max-pages <n>', 'Maximum number of pages to scan while deriving scenarios')
|
|
195
|
+
.option('--out <dir>', 'Directory to write the scaffolded project into', DEFAULT_OUT_DIR)
|
|
196
|
+
.option('--json', 'Do not write to disk — print the full scaffold result as JSON on stdout (use for MCP/CI)', false)
|
|
197
|
+
.option('--recipes <ids>', 'Comma-separated recipe ids to append proven test patterns: auth,a11y,nav,seed (e.g. --recipes auth,a11y)')
|
|
198
|
+
.option('--validate-specs', 'Fail (non-zero exit) if any generated spec does not parse/compile. Validation always runs; this makes a failure fatal.', false)
|
|
199
|
+
.action(async (options) => {
|
|
200
|
+
const parsedFramework = FrameworkSchema.safeParse(options.framework);
|
|
201
|
+
if (!parsedFramework.success) {
|
|
202
|
+
throw new Error(`Invalid --framework "${options.framework}". Supported: ${FRAMEWORKS.join(', ')}.`);
|
|
203
|
+
}
|
|
204
|
+
let maxPages;
|
|
205
|
+
if (options.maxPages !== undefined) {
|
|
206
|
+
const n = Number(options.maxPages);
|
|
207
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
208
|
+
throw new Error(`--max-pages must be a positive integer, got "${options.maxPages}".`);
|
|
209
|
+
}
|
|
210
|
+
maxPages = n;
|
|
211
|
+
}
|
|
212
|
+
let recipes;
|
|
213
|
+
if (options.recipes) {
|
|
214
|
+
const ids = options.recipes.split(',').map((s) => s.trim()).filter(Boolean);
|
|
215
|
+
const parsed = ids.map((id) => RecipeIdSchema.safeParse(id));
|
|
216
|
+
const invalid = parsed.map((p, i) => (!p.success ? ids[i] : null)).filter(Boolean);
|
|
217
|
+
if (invalid.length > 0) {
|
|
218
|
+
throw new Error(`Invalid --recipes value(s): ${invalid.join(', ')}. Supported: auth, a11y, nav, seed.`);
|
|
219
|
+
}
|
|
220
|
+
recipes = parsed.map((p) => (p.success ? p.data : null)).filter(Boolean);
|
|
221
|
+
}
|
|
222
|
+
await runScaffold({
|
|
223
|
+
url: options.url,
|
|
224
|
+
framework: parsedFramework.data,
|
|
225
|
+
maxPages,
|
|
226
|
+
out: options.out,
|
|
227
|
+
json: Boolean(options.json),
|
|
228
|
+
recipes,
|
|
229
|
+
validateSpecs: Boolean(options.validateSpecs),
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { AutomationMaturity } from '../schemas/automation-maturity.schema.js';
|
|
3
|
+
export interface ScoreAutomationOptions {
|
|
4
|
+
/** Path to the local repo to score (required). */
|
|
5
|
+
repo: string;
|
|
6
|
+
/** Emit the full AutomationMaturity object as JSON to stdout instead of the human report. */
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve `--repo` to an absolute path and assert it is an existing directory.
|
|
11
|
+
* Fails fast with a clear, actionable message rather than letting glob silently
|
|
12
|
+
* scan nothing and report a falsely-confident "everything is uncovered" score.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveRepoPath(repoOption: string | undefined, cwd?: string): string;
|
|
15
|
+
/** Build the human-readable report as a single string (kept pure so tests can assert on it). */
|
|
16
|
+
export declare function formatHumanReport(maturity: AutomationMaturity): string;
|
|
17
|
+
/**
|
|
18
|
+
* Core of the command, factored out of the action handler so node:test can drive it
|
|
19
|
+
* directly against a fixture repo without spawning a process.
|
|
20
|
+
*
|
|
21
|
+
* Reuses scanRepo (static repo intelligence) then computes maturity explicitly.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runScoreAutomation(options: ScoreAutomationOptions, out?: (line: string) => void): Promise<AutomationMaturity>;
|
|
24
|
+
export declare function registerScoreAutomationCommand(program: Command): void;
|
|
25
|
+
//# sourceMappingURL=score-automation-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score-automation-run.d.ts","sourceRoot":"","sources":["../../src/cli/score-automation-run.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,0CAA0C,CAAC;AAIlD,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAYnG;AA+BD,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAiBtE;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,sBAAsB,EAC/B,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAkC,GACxD,OAAO,CAAC,kBAAkB,CAAC,CAW7B;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAWrE"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `qulib score-automation` — score a local repo's test-automation maturity.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `computeAutomationMaturity(repo)` (../tools/scoring/automation-maturity.js)
|
|
5
|
+
* as a first-class CLI surface. Until now the maturity score was only produced as a
|
|
6
|
+
* side-effect of `analyze --repo`; this exposes it standalone so an agent or CI can
|
|
7
|
+
* score a repo's automation directly, without a deployed URL to crawl.
|
|
8
|
+
*
|
|
9
|
+
* How a RepoAnalysis is obtained (smallest honest path, no duplicated logic):
|
|
10
|
+
* `scanRepo(repoPath)` (../tools/repo/scan.js) is a pure static scan — it infers
|
|
11
|
+
* routes, test files, test-id hygiene, CI presence and Cypress structure straight
|
|
12
|
+
* from the repo layout, with no browser/URL dependency. We reuse it (root CLAUDE.md:
|
|
13
|
+
* shared logic lives in core, never duplicate) and then call computeAutomationMaturity
|
|
14
|
+
* on its result so the printed report reflects a freshly-computed maturity object.
|
|
15
|
+
*
|
|
16
|
+
* Output honesty (root design principle — no false confidence):
|
|
17
|
+
* Per-dimension applicability (`applicable | not_applicable | unknown`) is surfaced
|
|
18
|
+
* verbatim. A `not_applicable` or `unknown` dimension reads as honest uncertainty
|
|
19
|
+
* with its reason/guidance — it is NEVER rendered as a "0/100" that looks like a
|
|
20
|
+
* real failing score. The overall score is normalized over applicable dimensions
|
|
21
|
+
* only (see computeAutomationMaturity), so absent capabilities don't drag it down.
|
|
22
|
+
*
|
|
23
|
+
* This file owns the `score-automation` subcommand end-to-end and is registered from
|
|
24
|
+
* cli/index.ts via `registerScoreAutomationCommand(program)`, so this command's build
|
|
25
|
+
* never edits index.ts (avoids collision with the parallel scaffold command).
|
|
26
|
+
*/
|
|
27
|
+
import { resolve } from 'node:path';
|
|
28
|
+
import { existsSync, statSync } from 'node:fs';
|
|
29
|
+
import { scanRepo } from '../tools/repo/scan.js';
|
|
30
|
+
import { computeAutomationMaturity } from '../tools/scoring/automation-maturity.js';
|
|
31
|
+
/**
|
|
32
|
+
* Resolve `--repo` to an absolute path and assert it is an existing directory.
|
|
33
|
+
* Fails fast with a clear, actionable message rather than letting glob silently
|
|
34
|
+
* scan nothing and report a falsely-confident "everything is uncovered" score.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveRepoPath(repoOption, cwd = process.cwd()) {
|
|
37
|
+
if (!repoOption || !repoOption.trim()) {
|
|
38
|
+
throw new Error('score-automation requires --repo <path> pointing at a local repository to score.');
|
|
39
|
+
}
|
|
40
|
+
const abs = resolve(cwd, repoOption.trim());
|
|
41
|
+
if (!existsSync(abs)) {
|
|
42
|
+
throw new Error(`--repo path does not exist: ${abs}`);
|
|
43
|
+
}
|
|
44
|
+
if (!statSync(abs).isDirectory()) {
|
|
45
|
+
throw new Error(`--repo path is not a directory: ${abs}`);
|
|
46
|
+
}
|
|
47
|
+
return abs;
|
|
48
|
+
}
|
|
49
|
+
/** Single-letter glyph + word for a dimension's applicability, for the human report. */
|
|
50
|
+
function applicabilityTag(dim) {
|
|
51
|
+
const applicability = dim.applicability ?? 'applicable';
|
|
52
|
+
switch (applicability) {
|
|
53
|
+
case 'not_applicable':
|
|
54
|
+
return 'n/a';
|
|
55
|
+
case 'unknown':
|
|
56
|
+
return 'unknown';
|
|
57
|
+
default:
|
|
58
|
+
return 'applicable';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Render one dimension line. For applicable dimensions we show the score; for
|
|
63
|
+
* not_applicable / unknown we show the word and the reason — NOT a misleading "0".
|
|
64
|
+
*/
|
|
65
|
+
function formatDimensionLine(dim) {
|
|
66
|
+
const applicability = dim.applicability ?? 'applicable';
|
|
67
|
+
const weightPct = `${Math.round(dim.weight * 100)}%`;
|
|
68
|
+
const head = ` - ${dim.dimension} [w=${weightPct}]`;
|
|
69
|
+
if (applicability === 'applicable') {
|
|
70
|
+
return `${head}: ${dim.score}/100`;
|
|
71
|
+
}
|
|
72
|
+
// Honest-uncertainty rendering: surface the label + reason, never a bare 0.
|
|
73
|
+
const reason = dim.reason ? ` — ${dim.reason}` : '';
|
|
74
|
+
return `${head}: ${applicabilityTag(dim)} (excluded from overall)${reason}`;
|
|
75
|
+
}
|
|
76
|
+
/** Build the human-readable report as a single string (kept pure so tests can assert on it). */
|
|
77
|
+
export function formatHumanReport(maturity) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push(`[qulib] Automation maturity for ${maturity.repoPath}`);
|
|
80
|
+
lines.push(` overall: ${maturity.overallScore}/100 — ${maturity.label} (level ${maturity.level})`);
|
|
81
|
+
lines.push(' dimensions:');
|
|
82
|
+
for (const dim of maturity.dimensions) {
|
|
83
|
+
lines.push(formatDimensionLine(dim));
|
|
84
|
+
}
|
|
85
|
+
if (maturity.topRecommendations.length > 0) {
|
|
86
|
+
lines.push(' top recommendations:');
|
|
87
|
+
for (const rec of maturity.topRecommendations) {
|
|
88
|
+
lines.push(` • ${rec}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
lines.push(' top recommendations: none — applicable dimensions are at/above target.');
|
|
93
|
+
}
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Core of the command, factored out of the action handler so node:test can drive it
|
|
98
|
+
* directly against a fixture repo without spawning a process.
|
|
99
|
+
*
|
|
100
|
+
* Reuses scanRepo (static repo intelligence) then computes maturity explicitly.
|
|
101
|
+
*/
|
|
102
|
+
export async function runScoreAutomation(options, out = (line) => console.log(line)) {
|
|
103
|
+
const repoPath = resolveRepoPath(options.repo);
|
|
104
|
+
const repo = await scanRepo(repoPath);
|
|
105
|
+
const maturity = computeAutomationMaturity(repo);
|
|
106
|
+
if (options.json) {
|
|
107
|
+
out(JSON.stringify(maturity, null, 2));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
out(formatHumanReport(maturity));
|
|
111
|
+
}
|
|
112
|
+
return maturity;
|
|
113
|
+
}
|
|
114
|
+
export function registerScoreAutomationCommand(program) {
|
|
115
|
+
program
|
|
116
|
+
.command('score-automation')
|
|
117
|
+
.description("Score a local repo's test-automation maturity (overall + per-dimension, with honest applicability)")
|
|
118
|
+
.requiredOption('--repo <path>', 'Path to the local repository to score')
|
|
119
|
+
.option('--json', 'Emit the full AutomationMaturity object as JSON to stdout', false)
|
|
120
|
+
.action(async (options) => {
|
|
121
|
+
await runScoreAutomation({ repo: options.repo, json: Boolean(options.json) });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notquality DOGFOOD — real delivery signals fixture.
|
|
3
|
+
*
|
|
4
|
+
* P5 — qulib ingests notquality's own delivery signals → Release Confidence.
|
|
5
|
+
*
|
|
6
|
+
* PROVENANCE (all signals gathered 2026-06-04 via `gh` CLI against TapeshN/notquality):
|
|
7
|
+
*
|
|
8
|
+
* E2E run: gh run view 26931370208 -R TapeshN/notquality
|
|
9
|
+
* branch: notquality-app/prod-migrate
|
|
10
|
+
* sha: 5732ed5a37ba46c503d1319da83c5c6f4c8e5cb6
|
|
11
|
+
* workflow: E2E (playwright job, job ID 79451559555)
|
|
12
|
+
* completed: 2026-06-04T04:46:20Z (duration: ~6m4s)
|
|
13
|
+
* conclusion: success
|
|
14
|
+
* test files: 29 spec files on origin/main (git ls-tree -r origin/main)
|
|
15
|
+
* test cases: 194 total test() calls across 29 spec files
|
|
16
|
+
* fixme/skip: 26 test.fixme / test.skip markers
|
|
17
|
+
* active (non-fixme): 194 - 26 = 168 runnable tests
|
|
18
|
+
* all passed (CI conclusion: success)
|
|
19
|
+
*
|
|
20
|
+
* CI run: gh run view 26931370215 -R TapeshN/notquality
|
|
21
|
+
* branch: notquality-app/prod-migrate
|
|
22
|
+
* sha: 5732ed5a37ba46c503d1319da83c5c6f4c8e5cb6
|
|
23
|
+
* workflow: CI (validate job, job ID 79451559554)
|
|
24
|
+
* completed: 2026-06-04T04:46:20Z (duration: ~1m13s)
|
|
25
|
+
* conclusion: success
|
|
26
|
+
* steps: typecheck, lint, validate:bugs, prisma generate, build — all green
|
|
27
|
+
*
|
|
28
|
+
* PR: gh pr view 52 -R TapeshN/notquality --json number,url,reviewDecision,mergeable,statusCheckRollup
|
|
29
|
+
* title: "Run prisma migrate deploy before build on Vercel"
|
|
30
|
+
* branch: notquality-app/prod-migrate
|
|
31
|
+
* number: 52
|
|
32
|
+
* url: https://github.com/TapeshN/notquality/pull/52
|
|
33
|
+
* reviewDecision: "" (no review assigned — open PR, no blocking change requests)
|
|
34
|
+
* mergeable: MERGEABLE
|
|
35
|
+
* status checks: CI validate → SUCCESS, E2E playwright → SUCCESS, Vercel → SUCCESS
|
|
36
|
+
*
|
|
37
|
+
* Repo inventory (gh ls-tree -r origin/main, 2026-06-04):
|
|
38
|
+
* 29 spec files, 77 source .ts/.tsx files (excl. e2e), 19 route pages
|
|
39
|
+
* playwright.config.ts present (.github/workflows/e2e.yml present)
|
|
40
|
+
* CI: .github/workflows/ci.yml present (typecheck + lint + validate:bugs + build)
|
|
41
|
+
* auth: dual auth (iron-session playground + NextAuth platform)
|
|
42
|
+
* test-id hygiene: data-testid used in app/ + e2e/ (29 spec files use getByTestId)
|
|
43
|
+
* challenges: 1 seeded (legacy-bug-hunt-1); 16-card static list page (P5 truth-fix target)
|
|
44
|
+
*
|
|
45
|
+
* COLLECTION TIMESTAMP: 2026-06-04T09:00:00.000Z
|
|
46
|
+
*
|
|
47
|
+
* This fixture is intentionally FROZEN at this point in time. When real delivery
|
|
48
|
+
* signals are updated, create a NEW fixture version (fixture-v2.ts, etc.) and
|
|
49
|
+
* update the integration tests to use the latest. Never silently mutate this file —
|
|
50
|
+
* the provenance citation is load-bearing for the eval/audit trail.
|
|
51
|
+
*/
|
|
52
|
+
export declare const FIXTURE_COLLECTION_TS = "2026-06-04T09:00:00.000Z";
|
|
53
|
+
/**
|
|
54
|
+
* Real CI run data from notquality E2E workflow (run #26931370208).
|
|
55
|
+
* Source: gh run view 26931370208 -R TapeshN/notquality
|
|
56
|
+
*/
|
|
57
|
+
export declare const NOTQUALITY_E2E_RUN: {
|
|
58
|
+
/** ISO-8601 timestamp of run completion. */
|
|
59
|
+
readonly completedAt: "2026-06-04T04:46:20.000Z";
|
|
60
|
+
/** CI build step succeeded (typecheck, lint, validate:bugs, prisma generate, build). */
|
|
61
|
+
readonly buildPassed: true;
|
|
62
|
+
/**
|
|
63
|
+
* 168 runnable tests (194 total test() calls − 26 test.fixme/test.skip markers).
|
|
64
|
+
* All 168 passed in this run. Source: spec-file grep + run conclusion=success.
|
|
65
|
+
*/
|
|
66
|
+
readonly testsPassed: 168;
|
|
67
|
+
readonly testsFailed: 0;
|
|
68
|
+
readonly testsErrored: 0;
|
|
69
|
+
/**
|
|
70
|
+
* 26 tests marked test.fixme or test.skip across 16 spec files.
|
|
71
|
+
* These are known quarantined defects (color-contrast a11y, label regression,
|
|
72
|
+
* EVT-001, duplicate challenge-title) — intentional, not infra failures.
|
|
73
|
+
*/
|
|
74
|
+
readonly testsFlaky: 0;
|
|
75
|
+
readonly runUrl: "https://github.com/TapeshN/notquality/actions/runs/26931370208";
|
|
76
|
+
readonly workflowName: "E2E (playwright)";
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Real CI validate-job data from notquality CI workflow (run #26931370215).
|
|
80
|
+
* Source: gh run view 26931370215 -R TapeshN/notquality
|
|
81
|
+
*/
|
|
82
|
+
export declare const NOTQUALITY_CI_RUN: {
|
|
83
|
+
readonly completedAt: "2026-06-04T04:46:20.000Z";
|
|
84
|
+
readonly buildPassed: true;
|
|
85
|
+
readonly testsPassed: 0;
|
|
86
|
+
readonly testsFailed: 0;
|
|
87
|
+
readonly testsErrored: 0;
|
|
88
|
+
readonly runUrl: "https://github.com/TapeshN/notquality/actions/runs/26931370215";
|
|
89
|
+
readonly workflowName: "CI (validate)";
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Real PR #52 metadata from notquality.
|
|
93
|
+
* Source: gh pr view 52 -R TapeshN/notquality --json number,url,reviewDecision,mergeable,statusCheckRollup
|
|
94
|
+
*/
|
|
95
|
+
export declare const NOTQUALITY_PR_52: {
|
|
96
|
+
readonly number: 52;
|
|
97
|
+
readonly url: "https://github.com/TapeshN/notquality/pull/52";
|
|
98
|
+
readonly reviewDecision: null;
|
|
99
|
+
readonly mergeable: "MERGEABLE";
|
|
100
|
+
readonly statusCheckRollup: readonly [{
|
|
101
|
+
readonly state: "SUCCESS";
|
|
102
|
+
readonly name: "validate";
|
|
103
|
+
readonly targetUrl: "https://github.com/TapeshN/notquality/actions/runs/26931370215/job/79451559554";
|
|
104
|
+
}, {
|
|
105
|
+
readonly state: "SUCCESS";
|
|
106
|
+
readonly name: "playwright";
|
|
107
|
+
readonly targetUrl: "https://github.com/TapeshN/notquality/actions/runs/26931370208/job/79451559555";
|
|
108
|
+
}, {
|
|
109
|
+
readonly state: "SUCCESS";
|
|
110
|
+
readonly name: "Vercel";
|
|
111
|
+
readonly targetUrl: "https://vercel.com/tapeshnagarwal-7364s-projects/notquality-app/5mSLRhKKEdwvnqMoTY4XWxSePFYB";
|
|
112
|
+
}, {
|
|
113
|
+
readonly state: "SUCCESS";
|
|
114
|
+
readonly name: "Vercel Preview Comments";
|
|
115
|
+
readonly targetUrl: "https://vercel.com/github";
|
|
116
|
+
}];
|
|
117
|
+
readonly noPr: false;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Repo-level automation maturity facts (from static scan of origin/main, 2026-06-04).
|
|
121
|
+
* These facts drive the test-automation EvidenceItem used when qulib_score_automation
|
|
122
|
+
* is held (no live local scan in this fixture — see HELD note below).
|
|
123
|
+
*
|
|
124
|
+
* HELD: The live qulib_score_automation(repoPath) call requires the notquality
|
|
125
|
+
* repo to be available at an absolute path on the build machine and needs the full
|
|
126
|
+
* qulib CLI. The integration test uses these pre-scored facts instead of a live scan.
|
|
127
|
+
* The live scan is operator-gated (run `qulib score-automation <path>` locally).
|
|
128
|
+
*/
|
|
129
|
+
export declare const NOTQUALITY_AUTOMATION_MATURITY: {
|
|
130
|
+
/**
|
|
131
|
+
* Estimated overall automation maturity score (0–100).
|
|
132
|
+
* Basis: Playwright present (framework-adoption ✓), 29 spec files covering 19 routes
|
|
133
|
+
* (test-coverage-breadth ~72%), e2e.yml + ci.yml present (ci-integration ✓),
|
|
134
|
+
* auth-test-coverage present (iron-session + NextAuth both tested ✓),
|
|
135
|
+
* data-testid used in spec files (test-id-hygiene present but not perfect — duplicate
|
|
136
|
+
* challenge-title test-id is a known defect, P5 truth-fix target), no component/unit
|
|
137
|
+
* tests yet (component-test-ratio = 0). Estimated L3 (60–79 range).
|
|
138
|
+
* Conservative estimate: 65/100.
|
|
139
|
+
*/
|
|
140
|
+
readonly overallScore: 65;
|
|
141
|
+
readonly level: 3;
|
|
142
|
+
readonly label: "L3 — building maturity";
|
|
143
|
+
/**
|
|
144
|
+
* Key facts cited (no live scan was run; these are statically-derived):
|
|
145
|
+
* - framework: Playwright (playwright.config.ts present on origin/main)
|
|
146
|
+
* - specFiles: 29 (git ls-tree origin/main | grep "e2e/" | grep "spec.ts")
|
|
147
|
+
* - routePages: 19 (git ls-tree origin/main | grep "app/" | grep "page.tsx")
|
|
148
|
+
* - ciWorkflows: 2 (.github/workflows/ci.yml + e2e.yml)
|
|
149
|
+
* - authTests: yes (e2e/specs/auth/ — 3 spec files, 18 test() calls)
|
|
150
|
+
* - testIdHygiene: partial (data-testid used; duplicate challenge-title known defect)
|
|
151
|
+
* - componentTests: 0 (no vitest/jest unit tests in repo)
|
|
152
|
+
*/
|
|
153
|
+
readonly topRecommendations: readonly ["Add vitest/jest unit tests for lib/ (scoring, API helpers) to raise component-test-ratio.", "Fix duplicate data-testid=\"challenge-title\" (breadcrumb vs h1) for unambiguous E2E selectors.", "Add dedicated challenges list + attempt E2E specs once fake list page is replaced with DB truth."];
|
|
154
|
+
readonly computedAt: "2026-06-04T09:00:00.000Z";
|
|
155
|
+
readonly repoPath: "TapeshN/notquality@5732ed5a (origin/main, 2026-06-04)";
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Repository context for the ConfidenceSubject.
|
|
159
|
+
* ref: the commit SHA + branch context; tenantId: 'notquality' (multi-tenant from day one).
|
|
160
|
+
*/
|
|
161
|
+
export declare const NOTQUALITY_SUBJECT: {
|
|
162
|
+
readonly kind: "release";
|
|
163
|
+
readonly ref: "TapeshN/notquality@5732ed5a (notquality-app/prod-migrate)";
|
|
164
|
+
readonly tenantId: "notquality";
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../../../src/examples/notquality-dogfood/fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,eAAO,MAAM,qBAAqB,6BAA6B,CAAC;AAEhE;;;GAGG;AACH,eAAO,MAAM,kBAAkB;IAC7B,4CAA4C;;IAE5C,wFAAwF;;IAExF;;;OAGG;;;;IAIH;;;;OAIG;;;;CAIK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;CAQpB,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;6BAGH,IAAI;;;;;;;;;;;;;;;;;;;;CAyBpB,CAAC;AAEX;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B;IACzC;;;;;;;;;OASG;;;;IAIH;;;;;;;;;OASG;;;;CAQK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;CAIrB,CAAC"}
|