qaify-playwright-reporter 0.8.0 → 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.
@@ -82,6 +82,13 @@ export interface SourceLocation {
82
82
  export declare function auditStatic(dir: string, deps: StaticDeps): Promise<StaticAudit>;
83
83
  /** Bentuk keluaran `claude -p --output-format json` agar parseAuditOutput / audit-submit tidak berubah. */
84
84
  export declare function toAuditOutput(audit: StaticAudit, durationMs: number): string;
85
+ /** Advisory composer audit / npm audit (berkas lockfile). */
86
+ export declare const isAdvisory: (f: AuditFinding) => boolean;
87
+ /**
88
+ * Urut severity; di severity yang sama temuan dari menjalankan aplikasi didahulukan sebelum advisory dependensi
89
+ * (sort stabil), dan advisory dibatasi MAX_ADVISORIES agar tidak memenuhi MAX_FINDINGS.
90
+ */
91
+ export declare function selectFindings(all: AuditFinding[]): AuditFinding[];
85
92
  /**
86
93
  * Jelajah link same-origin (BFS) di desktop, lalu ulangi halaman yang sama di mobile.
87
94
  * Setiap halaman difoto polos (galeri laporan); setiap jenis masalah difoto lagi dengan tanda (kotak merah di elemen,
@@ -98,14 +105,10 @@ export declare function smokeUi(browser: BrowserLike, baseUrl: string, evidenceD
98
105
  }>;
99
106
  /** Test gagal / error dari laporan JUnit PHPUnit / Pest. */
100
107
  export declare function parseJunit(xml: string, root?: string): AuditFinding[];
101
- /** `composer audit --format=json` → satu temuan per advisory. */
108
+ /** `composer audit --format=json` → satu temuan per paket (advisory duplikat digabung, severity terparah). */
102
109
  export declare function parseComposerAudit(output: string): AuditFinding[];
103
110
  /** `npm audit --json` (npm 7+) → satu temuan per paket berseverity high/critical. */
104
111
  export declare function parseNpmAudit(output: string): AuditFinding[];
105
- /** URI route GET tanpa parameter dan tanpa middleware autentikasi dari `route:list --json`. */
106
- export declare function pickSmokeRoutes(output: string): string[];
107
- /** URI → action (mis. App\Http\Controllers\ProdukController@index) dari `route:list --json`. */
108
- export declare function routeActions(output: string): Map<string, string>;
109
112
  /**
110
113
  * Request FE (method + path, mis. GET /api/produk/12) → action route BE dari `route:list --json`.
111
114
  * {param} cocok dengan satu segmen, {param?} opsional; bila beberapa cocok, route dengan segmen statis terbanyak menang.
@@ -120,17 +123,23 @@ export declare function controllerLocation(root: string, action: string): Promis
120
123
  * 2. routing berbasis berkas (pages/, app/ ala Nuxt / Next).
121
124
  */
122
125
  export declare function resolveRouteFile(root: string, path: string): Promise<SourceLocation | null>;
126
+ export declare function listSources(dir: string, depth: number): Promise<string[]>;
127
+ export declare const lineAt: (source: string, index: number) => number;
128
+ /** 100 dikurangi penalti severity; penalti advisory dependensi dibatasi MAX_ADVISORY_PENALTY. */
123
129
  export declare function scoreRepo(findings: AuditFinding[], booted: boolean): number;
124
130
  export interface Login {
125
131
  email: string;
126
132
  password: string;
127
133
  }
128
- /** Akun login dari catatan aplikasi, mis. "Login admin@pos.test / password". */
134
+ /** Akun login pertama dari catatan aplikasi, mis. "Login admin@pos.test / password" (smoke UI). */
129
135
  export declare function parseLogin(instructions: string): Login | null;
130
136
  /** Variabel VITE_* berisi URL http diarahkan ke BE lokal; path (mis. /api) dipertahankan. */
131
137
  export declare function localApiEnv(example: string): string;
132
138
  /** Ganti / tambah KEY=VALUE di isi file .env. */
133
139
  export declare function setEnv(content: string, values: Record<string, string>): string;
140
+ export declare const tail: (text: string, max?: number) => string;
141
+ /** Keluaran composer/npm kadang diawali peringatan; ambil dari karakter JSON pertama. */
142
+ export declare function parseJsonLoose(output: string): any;
134
143
  export declare const defaultShell: Shell;
135
144
  /** playwright-core dipasang global di runner bersama qaify; Google Chrome sudah ada di ubuntu-latest. */
136
145
  export declare function defaultLaunch(): Promise<BrowserLike | null>;
@@ -1,21 +1,27 @@
1
1
  // Audit aplikasi tanpa AI (qaify audit-static): pemeriksaan deterministik di repo runner bila
2
2
  // CLAUDE_CODE_OAUTH_TOKEN kosong. Hasilnya berbentuk sama dengan keluaran `claude -p`, jadi audit-submit,
3
- // backend, dan UI tidak berubah. Temuan lebih dangkal dari mode Claude: tidak ada analisis logika bisnis
4
- // atau kontrak API dari membaca kode.
5
- // BE (Laravel): composer install, migrate --seed, php artisan test, composer audit, smoke route GET publik.
3
+ // backend, dan UI tidak berubah. Temuan lebih dangkal dari mode Claude: aturan bisnis tidak ditebak dari kode,
4
+ // hanya gejalanya yang diuji lewat HTTP (audit-api.ts).
5
+ // BE (Laravel): composer install, migrate --seed, php artisan test, composer audit, smoke API (dengan login bila ada
6
+ // akun), uji validasi POST/PUT/PATCH, uji otorisasi.
6
7
  // FE (Node): npm ci, npm run build, npm audit, smoke Playwright (desktop + mobile) dengan screenshot setiap halaman,
7
- // plus foto bertanda (kotak merah di elemen bermasalah / banner pesan error) untuk setiap masalah.
8
+ // plus foto bertanda (kotak merah di elemen bermasalah / banner pesan error) untuk setiap masalah, dan cek kontrak
9
+ // pemanggilan API di kode FE terhadap route BE.
8
10
  // Temuan smoke diberi berkas sumber bila bisa ditebak: route FE → komponen halaman, route BE → method controller;
9
11
  // request FE yang gagal juga dipetakan ke controller BE, dan route BE yang error ke halaman FE pemanggilnya.
10
12
  import { spawn } from 'node:child_process';
11
13
  import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
12
14
  import { openSync } from 'node:fs';
13
15
  import { dirname, join, relative } from 'node:path';
16
+ import { checkAuthorization, checkContract, checkValidation, openApi, parseLogins, parseRoutes, smokeApi } from './audit-api.js';
14
17
  /** usage.model untuk audit tanpa AI; dipakai backend/PDF untuk menampilkan mode audit. */
15
18
  export const STATIC_MODEL = 'qaify-static';
16
19
  const MAX_FINDINGS = 40;
20
+ /** Advisory composer/npm audit yang masuk laporan, agar tidak menyingkirkan temuan dari menjalankan aplikasi. */
21
+ const MAX_ADVISORIES = 10;
22
+ /** Batas total penalti skor dari advisory dependensi. */
23
+ const MAX_ADVISORY_PENALTY = 30;
17
24
  const MAX_SMOKE_PAGES = 15;
18
- const MAX_SMOKE_ROUTES = 40;
19
25
  /** Foto bertanda per halaman per viewport (satu per jenis masalah). */
20
26
  const MAX_ISSUE_SHOTS = 3;
21
27
  const BE_URL = 'http://127.0.0.1:8000';
@@ -32,25 +38,49 @@ export async function auditStatic(dir, deps) {
32
38
  await mkdir(join(dir, '.qaify'), { recursive: true });
33
39
  await mkdir(join(dir, 'evidence'), { recursive: true });
34
40
  const context = await readFile(join(dir, 'context.json'), 'utf8').then(JSON.parse, () => ({}));
35
- const login = parseLogin(String(context?.project?.instructions ?? ''));
41
+ const accounts = parseLogins(String(context?.project?.instructions ?? ''));
42
+ const login = accounts[0] ? { email: accounts[0].email, password: accounts[0].password } : null;
36
43
  const beRoot = join(dir, 'apps', 'be');
37
44
  const feRoot = join(dir, 'apps', 'fe');
38
45
  const be = await auditBe(beRoot, dir, deps);
46
+ const routes = parseRoutes(be.routes);
39
47
  const api = be.routes
40
48
  ? async (method, path) => {
41
49
  const action = matchRoute(be.routes, method, path);
42
50
  return action ? controllerLocation(beRoot, action) : null;
43
51
  }
44
52
  : undefined;
53
+ let probe = null;
54
+ let failed = [];
55
+ if (be.running) {
56
+ deps.log('qa-factory: [be] smoke API');
57
+ const opened = await openApi(routes, accounts, { base: BE_URL, fetch: deps.fetch, root: beRoot });
58
+ probe = opened.probe;
59
+ const smoke = await smokeApi(probe);
60
+ be.result.findings.push(...smoke.findings);
61
+ be.result.notes.push(...opened.notes, ...smoke.notes);
62
+ failed = smoke.failed;
63
+ }
45
64
  let fe;
46
65
  try {
47
66
  fe = await auditFe(feRoot, dir, deps, login, api);
67
+ // mengubah data: setelah smoke FE agar halaman FE tidak melihat data uji
68
+ if (probe) {
69
+ deps.log('qa-factory: [be] uji validasi & otorisasi');
70
+ for (const check of [await checkValidation(probe), await checkAuthorization(probe, beRoot)]) {
71
+ be.result.findings.push(...check.findings);
72
+ be.result.notes.push(...check.notes);
73
+ }
74
+ }
48
75
  }
49
76
  finally {
50
77
  be.stop();
51
78
  }
52
- await linkCallers(be.failed, fe, feRoot);
53
- const findings = [...be.result.findings, ...fe.findings].sort(bySeverity).slice(0, MAX_FINDINGS);
79
+ const contract = await checkContract(feRoot, beRoot, routes);
80
+ fe.findings.push(...contract.findings);
81
+ fe.notes.push(...contract.notes);
82
+ await linkCallers(failed, fe, feRoot);
83
+ const findings = selectFindings([...be.result.findings, ...fe.findings]);
54
84
  return {
55
85
  summary: { be: summarize('BE', be.result), fe: summarize('FE', fe) },
56
86
  findings,
@@ -68,6 +98,16 @@ export function toAuditOutput(audit, durationMs) {
68
98
  modelUsage: { [STATIC_MODEL]: { outputTokens: 0 } },
69
99
  });
70
100
  }
101
+ /** Advisory composer audit / npm audit (berkas lockfile). */
102
+ export const isAdvisory = (f) => f.area === 'security' && (f.file === 'composer.lock' || f.file === 'package-lock.json');
103
+ /**
104
+ * Urut severity; di severity yang sama temuan dari menjalankan aplikasi didahulukan sebelum advisory dependensi
105
+ * (sort stabil), dan advisory dibatasi MAX_ADVISORIES agar tidak memenuhi MAX_FINDINGS.
106
+ */
107
+ export function selectFindings(all) {
108
+ const advisories = all.filter(isAdvisory).sort(bySeverity).slice(0, MAX_ADVISORIES);
109
+ return [...all.filter((f) => !isAdvisory(f)), ...advisories].sort(bySeverity).slice(0, MAX_FINDINGS);
110
+ }
71
111
  /**
72
112
  * Route BE yang error saat smoke API → halaman FE yang memanggilnya: halaman jadi `page`, komponennya masuk
73
113
  * related_files, dan foto halaman itu jadi bukti agar temuan BE juga tampak di laporan.
@@ -96,7 +136,7 @@ async function linkCallers(failed, fe, feRoot) {
96
136
  }
97
137
  async function auditBe(root, dir, deps) {
98
138
  const result = { findings: [], booted: false, notes: [] };
99
- const none = { result, stop: () => { }, routes: '', failed: [] };
139
+ const none = { result, stop: () => { }, routes: '', running: false };
100
140
  const { shell, log } = deps;
101
141
  if (!(await exists(root))) {
102
142
  result.notes.push('Repo BE tidak ada di folder kerja.');
@@ -173,31 +213,7 @@ async function auditBe(root, dir, deps) {
173
213
  }
174
214
  result.booted = migrated;
175
215
  const routes = await shell.run('php artisan route:list --json --no-interaction', { cwd: root, timeoutMs: MIN });
176
- const smoke = pickSmokeRoutes(routes.output);
177
- const actions = routeActions(routes.output);
178
- const failed = [];
179
- for (const uri of smoke) {
180
- const url = `${BE_URL}/${uri.replace(/^\//, '')}`;
181
- const res = await deps.fetch(url, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(10_000) }).catch(() => null);
182
- if (res && res.status >= 500) {
183
- const action = actions.get(uri);
184
- const location = action ? await controllerLocation(root, action) : null;
185
- const finding = {
186
- repo: 'be',
187
- area: 'error_handling',
188
- severity: 'high',
189
- title: `GET /${uri} membalas ${res.status}`,
190
- description: `Route publik GET /${uri}${action ? ` (${action})` : ''} membalas ${res.status} pada database hasil seed.`,
191
- ...location,
192
- evidence: { request: `GET ${url}`, response: tail(await res.text().catch(() => ''), 2000) },
193
- suggestion: 'Cek laravel.log untuk exception di route ini dan tangani kasus data kosong / parameter yang hilang.',
194
- };
195
- result.findings.push(finding);
196
- failed.push({ finding, uri });
197
- }
198
- }
199
- result.notes.push(`Smoke API: ${smoke.length} route GET publik dipanggil.`);
200
- return { result, stop, routes: routes.code === 0 ? routes.output : '', failed };
216
+ return { result, stop, routes: routes.code === 0 ? routes.output : '', running: true };
201
217
  }
202
218
  // ---------------------------------------------------------------- FE
203
219
  async function auditFe(root, dir, deps, login, api) {
@@ -625,26 +641,50 @@ export function parseJunit(xml, root = '') {
625
641
  }
626
642
  return findings;
627
643
  }
628
- /** `composer audit --format=json` → satu temuan per advisory. */
644
+ /** "guzzlehttp/psr7: Host confusion" → "Host confusion" (judul advisory sering sudah diawali nama paket). */
645
+ const stripPackage = (pkg, title) => title.replace(new RegExp(`^${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:\\s*`, 'i'), '');
646
+ /** Advisory sama (CVE sama, atau judul sama setelah kata nama paket dibuang) digabung; severity tak dikenal → medium. */
647
+ function dedupeAdvisories(pkg, list) {
648
+ const words = new Set(pkg.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
649
+ const out = [];
650
+ for (const raw of list) {
651
+ const title = stripPackage(pkg, String(raw?.title ?? 'kerentanan diketahui'));
652
+ const cve = typeof raw?.cve === 'string' && raw.cve ? raw.cve : null;
653
+ const severity = SEVERITY_ORDER.includes(raw?.severity) ? String(raw.severity) : 'medium';
654
+ const key = title.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w && !words.has(w)).join(' ');
655
+ const same = out.find((a) => (cve && a.cve === cve) || a.key === key);
656
+ if (same) {
657
+ if (rank(severity) < rank(same.severity))
658
+ same.severity = severity;
659
+ same.cve ??= cve;
660
+ continue;
661
+ }
662
+ out.push({ title, cve, severity, affectedVersions: raw?.affectedVersions, link: raw?.link, key });
663
+ }
664
+ return out;
665
+ }
666
+ const rank = (severity) => SEVERITY_ORDER.indexOf(severity);
667
+ /** `composer audit --format=json` → satu temuan per paket (advisory duplikat digabung, severity terparah). */
629
668
  export function parseComposerAudit(output) {
630
669
  const data = parseJsonLoose(output);
631
670
  const advisories = data?.advisories && typeof data.advisories === 'object' ? data.advisories : {};
632
671
  const findings = [];
633
672
  for (const [pkg, list] of Object.entries(advisories)) {
634
- for (const advisory of Array.isArray(list) ? list : Object.values(list ?? {})) {
635
- const severity = SEVERITY_ORDER.includes(advisory?.severity) ? advisory.severity : 'high';
636
- findings.push({
637
- repo: 'be',
638
- area: 'security',
639
- severity,
640
- title: `${pkg}: ${advisory?.title ?? 'kerentanan diketahui'}`.slice(0, 120),
641
- description: [advisory?.title, advisory?.cve && `CVE: ${advisory.cve}`, advisory?.affectedVersions && `Versi terdampak: ${advisory.affectedVersions}`, advisory?.link]
642
- .filter(Boolean)
643
- .join('\n'),
644
- file: 'composer.lock',
645
- suggestion: `composer update ${pkg} ke versi yang sudah diperbaiki.`,
646
- });
647
- }
673
+ const unique = dedupeAdvisories(pkg, Array.isArray(list) ? list : Object.values(list ?? {}));
674
+ if (!unique.length)
675
+ continue;
676
+ const worst = [...unique].sort((a, b) => rank(a.severity) - rank(b.severity))[0];
677
+ findings.push({
678
+ repo: 'be',
679
+ area: 'security',
680
+ severity: worst.severity,
681
+ title: (unique.length > 1 ? `${pkg}: ${unique.length} kerentanan — ${worst.title}` : `${pkg}: ${worst.title}`).slice(0, 120),
682
+ description: unique
683
+ .map((a) => `- [${a.severity}] ${a.title}${a.cve ? ` (${a.cve})` : ''}${a.affectedVersions ? ` · versi terdampak ${a.affectedVersions}` : ''}${a.link ? ` · ${a.link}` : ''}`)
684
+ .join('\n'),
685
+ file: 'composer.lock',
686
+ suggestion: `composer update ${pkg} --with-all-dependencies ke versi yang sudah diperbaiki.`,
687
+ });
648
688
  }
649
689
  return findings;
650
690
  }
@@ -661,7 +701,7 @@ export function parseNpmAudit(output) {
661
701
  repo: 'fe',
662
702
  area: 'security',
663
703
  severity: vuln.severity,
664
- title: `${name}: ${advisories[0]?.title ?? 'kerentanan diketahui'}`.slice(0, 120),
704
+ title: `${name}: ${stripPackage(name, advisories[0]?.title ?? 'kerentanan diketahui')}`.slice(0, 120),
665
705
  description: [
666
706
  ...advisories.slice(0, 3).map((a) => `${a.title}${a.url ? ` (${a.url})` : ''}`),
667
707
  vuln.range && `Versi terdampak: ${vuln.range}`,
@@ -675,28 +715,6 @@ export function parseNpmAudit(output) {
675
715
  }
676
716
  return findings;
677
717
  }
678
- /** URI route GET tanpa parameter dan tanpa middleware autentikasi dari `route:list --json`. */
679
- export function pickSmokeRoutes(output) {
680
- const routes = parseJsonLoose(output);
681
- if (!Array.isArray(routes))
682
- return [];
683
- const uris = routes
684
- .filter((r) => String(r?.method ?? '').split('|').includes('GET'))
685
- .filter((r) => typeof r?.uri === 'string' && !r.uri.includes('{') && !/^(_|telescope|horizon|pulse|livewire|sanctum|storage)/.test(r.uri))
686
- .filter((r) => !(Array.isArray(r?.middleware) ? r.middleware : []).some((m) => /^(auth|verified|signed|can:|password\.confirm)/.test(String(m).split('\\').pop() ?? '') || /Authenticate/.test(String(m))))
687
- .map((r) => String(r.uri));
688
- return [...new Set(uris)].slice(0, MAX_SMOKE_ROUTES);
689
- }
690
- /** URI → action (mis. App\Http\Controllers\ProdukController@index) dari `route:list --json`. */
691
- export function routeActions(output) {
692
- const routes = parseJsonLoose(output);
693
- const actions = new Map();
694
- for (const r of Array.isArray(routes) ? routes : []) {
695
- if (typeof r?.uri === 'string' && typeof r?.action === 'string' && r.action !== 'Closure' && !actions.has(r.uri))
696
- actions.set(r.uri, r.action);
697
- }
698
- return actions;
699
- }
700
718
  /**
701
719
  * Request FE (method + path, mis. GET /api/produk/12) → action route BE dari `route:list --json`.
702
720
  * {param} cocok dengan satu segmen, {param?} opsional; bila beberapa cocok, route dengan segmen statis terbanyak menang.
@@ -807,7 +825,7 @@ async function componentNear(root, file, source, index) {
807
825
  }
808
826
  return null;
809
827
  }
810
- async function listSources(dir, depth) {
828
+ export async function listSources(dir, depth) {
811
829
  if (depth < 0)
812
830
  return [];
813
831
  const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
@@ -823,15 +841,18 @@ async function listSources(dir, depth) {
823
841
  }
824
842
  return out;
825
843
  }
826
- const lineAt = (source, index) => source.slice(0, index).split('\n').length;
844
+ export const lineAt = (source, index) => source.slice(0, index).split('\n').length;
845
+ /** 100 dikurangi penalti severity; penalti advisory dependensi dibatasi MAX_ADVISORY_PENALTY. */
827
846
  export function scoreRepo(findings, booted) {
828
- const score = Math.max(0, 100 - findings.reduce((sum, f) => sum + (PENALTY[f.severity] ?? 0), 0));
847
+ const penalty = (list) => list.reduce((sum, f) => sum + (PENALTY[f.severity] ?? 0), 0);
848
+ const advisories = findings.filter(isAdvisory);
849
+ const score = Math.max(0, 100 - penalty(findings.filter((f) => !isAdvisory(f))) - Math.min(MAX_ADVISORY_PENALTY, penalty(advisories)));
829
850
  return booted ? score : Math.min(score, 40);
830
851
  }
831
- /** Akun login dari catatan aplikasi, mis. "Login admin@pos.test / password". */
852
+ /** Akun login pertama dari catatan aplikasi, mis. "Login admin@pos.test / password" (smoke UI). */
832
853
  export function parseLogin(instructions) {
833
- const match = /([\w.+-]+@[\w-]+(?:\.[\w-]+)+)\s*\/\s*(\S+)/.exec(instructions);
834
- return match ? { email: match[1], password: match[2] } : null;
854
+ const account = parseLogins(instructions)[0];
855
+ return account ? { email: account.email, password: account.password } : null;
835
856
  }
836
857
  /** Variabel VITE_* berisi URL http diarahkan ke BE lokal; path (mis. /api) dipertahankan. */
837
858
  export function localApiEnv(example) {
@@ -874,7 +895,7 @@ function bootFailure(repo, title, description, output, file) {
874
895
  };
875
896
  }
876
897
  const bySeverity = (a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity);
877
- const tail = (text, max = 4000) => (text.length > max ? `…${text.slice(-max)}` : text);
898
+ export const tail = (text, max = 4000) => (text.length > max ? `…${text.slice(-max)}` : text);
878
899
  const exists = (path) => stat(path).then(() => true, () => false);
879
900
  function pathOf(href, origin) {
880
901
  try {
@@ -912,7 +933,7 @@ function relativeTo(file, root) {
912
933
  return base && normalized.startsWith(`${base}/`) ? normalized.slice(base.length + 1) : normalized.replace(/^.*?\/(tests|app|src)\//, '$1/');
913
934
  }
914
935
  /** Keluaran composer/npm kadang diawali peringatan; ambil dari karakter JSON pertama. */
915
- function parseJsonLoose(output) {
936
+ export function parseJsonLoose(output) {
916
937
  const start = output.search(/[[{]/);
917
938
  if (start < 0)
918
939
  return null;
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@
16
16
  // Perbaikan temuan audit (qaify-fix.yml di repo runner):
17
17
  // qaify fix-context --fix <id> --dir <folder kerja> (job fix: clone repo target, prompt Claude)
18
18
  // qaify fix-schema --out schema.json --instructions instructions.txt
19
+ // qaify fix-deps --dir <folder kerja> (job fix, mode deps: update lockfile tanpa Claude)
19
20
  // qaify fix-diff --dir <folder kerja> --out <folder artefak> (job fix: perubahan Claude → changes.patch)
20
21
  // qaify fix-submit --fix <id> --results claude.json --patch changes.patch --dir <folder kosong> (job submit)
21
22
  // Auto-fix test yang gagal (auto-fix.yml di repo yang diuji, dibuat `qaify init`); fix-diff dipakai bersama:
@@ -29,7 +30,7 @@ import { aiQueue, aiSubmit, analyze, CONCLUSIONS, parseArgs } from './analyze.js
29
30
  import { AUDIT_INSTRUCTIONS, AUDIT_PROMPT, AUDIT_SCHEMA, auditContext, auditSubmit, readCommits, readContextError } from './audit.js';
30
31
  import { auditStatic, defaultLaunch, defaultShell, toAuditOutput } from './audit-static.js';
31
32
  import { createClient, describeProblem, MISSING_CONFIG, runFromEnv, warn } from './client.js';
32
- import { FIX_INSTRUCTIONS, FIX_SCHEMA, fixContext, fixDiff, fixSubmit, TEST_FIX_INSTRUCTIONS, writeOutputs } from './fix.js';
33
+ import { FIX_INSTRUCTIONS, FIX_SCHEMA, fixContext, fixDeps, fixDiff, fixSubmit, TEST_FIX_INSTRUCTIONS, writeOutputs } from './fix.js';
33
34
  import { init, initRunner, NEXT_STEPS, RUNNER_NEXT_STEPS } from './init.js';
34
35
  import { check } from './check.js';
35
36
  const USAGE = `Pemakaian:
@@ -46,6 +47,7 @@ const USAGE = `Pemakaian:
46
47
  qaify audit-submit --audit <id> --results <file> --evidence <folder> --context <file>
47
48
  qaify fix-context --fix <id> --dir <folder>
48
49
  qaify fix-schema --out <file-schema> [--instructions <file>]
50
+ qaify fix-deps --dir <folder>
49
51
  qaify fix-diff --dir <folder> --out <folder>
50
52
  qaify fix-submit --fix <id> --results <file> --patch <file> --dir <folder>
51
53
  qaify autofix-context --fix <id> --dir <folder>
@@ -195,13 +197,27 @@ switch (args.command) {
195
197
  }
196
198
  const result = client.enabled
197
199
  ? await fixContext(client, args.fix, args.dir, { kind: fixKind(args.command), githubRunId: run.github_run_id || undefined })
198
- : (warn(MISSING_CONFIG), { ok: false, model: null });
200
+ : (warn(MISSING_CONFIG), { ok: false, model: null, mode: 'claude' });
199
201
  if (!result.ok) {
200
202
  const problem = describeProblem(client);
201
203
  if (problem)
202
204
  warn(problem);
203
205
  }
204
- await writeOutputs({ ok: String(result.ok), model: result.model ?? '' });
206
+ await writeOutputs({ ok: String(result.ok), model: result.model ?? '', mode: result.mode });
207
+ break;
208
+ }
209
+ // job fix, tanpa token dan tanpa Claude: update advisory dependensi di lockfile
210
+ case 'fix-deps': {
211
+ if (!args.dir) {
212
+ usage();
213
+ break;
214
+ }
215
+ const result = await fixDeps(args.dir).catch((e) => {
216
+ warn(`update dependensi gagal: ${e.message.split('\n')[0]}`);
217
+ return null;
218
+ });
219
+ if (result)
220
+ console.log(`qa-factory: ${result.fixed ? 'dependensi diperbarui' : `dependensi tidak diperbarui: ${result.reason ?? result.summary}`}`);
205
221
  break;
206
222
  }
207
223
  case 'fix-schema':
package/dist/fix.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { type Exec } from './audit.js';
2
2
  import type { IngestClient } from './client.js';
3
3
  import { type ClaudeUsage } from './usage.js';
4
+ /** "Model" perbaikan advisory dependensi tanpa AI; cermin config qafactory.audit_fix.deps_model. */
5
+ export declare const DEPS_MODEL = "qaify-deps";
4
6
  export declare const FIX_SCHEMA: {
5
7
  readonly type: "object";
6
8
  readonly properties: {
@@ -59,6 +61,12 @@ interface FixContext {
59
61
  max_files: number;
60
62
  max_changed_lines: number;
61
63
  };
64
+ /** Advisory dependensi: diperbaiki fix-deps tanpa Claude. Backend lama tidak mengirimnya. */
65
+ deps?: DepsTarget | null;
66
+ }
67
+ export interface DepsTarget {
68
+ package: string;
69
+ manager: 'composer' | 'npm';
62
70
  }
63
71
  /** Prompt `claude -p`: temuan + batas + percobaan sebelumnya. Detail cara kerja di FIX_INSTRUCTIONS. */
64
72
  export declare function fixPrompt(ctx: Omit<FixContext, 'repo'> & {
@@ -108,6 +116,8 @@ export declare function testFixPrompt(ctx: Omit<TestFixContext, 'repo'> & {
108
116
  export interface FixContextResult {
109
117
  ok: boolean;
110
118
  model: string | null;
119
+ /** deps = langkah Claude dilewati, fix-deps yang memperbaiki. */
120
+ mode: 'claude' | 'deps';
111
121
  }
112
122
  /**
113
123
  * GET {base}/{id}/context → clone repo di commit sumber (yang diaudit / run yang gagal) ke <dir>/repo → cabut token
@@ -120,9 +130,22 @@ export declare function fixContext(client: IngestClient, fixId: string, dir: str
120
130
  fetch?: typeof fetch;
121
131
  log?: (msg: string) => void;
122
132
  }): Promise<FixContextResult>;
133
+ /**
134
+ * Advisory dependensi tanpa AI: update paket di lockfile saja (tanpa install / script), cek ulang dengan audit, lalu
135
+ * tulis hasilnya ke <dir>/.qaify/claude.json dalam bentuk keluaran `claude -p` (model qaify-deps, token 0) agar
136
+ * fix-diff / fix-submit tidak berubah. npm: npm update dulu, npm audit fix bila paket masih rentan.
137
+ */
138
+ export declare function fixDeps(dir: string, exec?: Exec): Promise<{
139
+ fixed: boolean;
140
+ summary: string;
141
+ reason?: string;
142
+ }>;
143
+ /** Masih ada advisory untuk paket ini? npm: hanya high / critical (sama dengan temuan audit-static). */
144
+ export declare function stillVulnerable(output: string, manager: 'composer' | 'npm', pkg: string): boolean;
123
145
  /**
124
146
  * Perubahan Claude di <dir>/repo → <out>/changes.patch (git diff --binary). Lockfile & .env yang ikut berubah saat
125
- * menjalankan aplikasi dibuang dari patch. Job ini tidak memegang token, jadi aman menjalankan git di repo hasil Claude.
147
+ * menjalankan aplikasi dibuang dari patch, kecuali lockfile pada mode deps (.qaify/deps.json) yang memang hasilnya.
148
+ * Job ini tidak memegang token, jadi aman menjalankan git di repo hasil Claude.
126
149
  */
127
150
  export declare function fixDiff(dir: string, out: string, exec?: Exec): Promise<{
128
151
  files: string[];
@@ -135,6 +158,7 @@ export interface PatchStats {
135
158
  paths: string[];
136
159
  /** Header diff yang tidak bisa dibaca: patch ditolak, bukan dilewati diam-diam. */
137
160
  unreadable: number;
161
+ /** Baris +/- di luar lockfile (lockfile hasil update paket wajar besar, tidak dihitung ke batas). */
138
162
  changedLines: number;
139
163
  }
140
164
  /** Berkas dan jumlah baris +/- dari unified diff git (path ber-escape git dibungkus tanda kutip). */