dw-kit 1.3.5 → 1.4.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/.claude/hooks/supply-chain-scan.sh +16 -14
- package/.claude/skills/dw-archive/SKILL.md +14 -0
- package/.claude/skills/dw-review/SKILL.md +33 -2
- package/.dw/config/config.schema.json +149 -121
- package/.dw/config/dw.config.yml +14 -0
- package/.dw/security/advisory-snapshot.json +157 -0
- package/.dw/security/ioc-namespaces.json +20 -8
- package/CLAUDE.md +1 -1
- package/README.md +15 -2
- package/package.json +2 -1
- package/src/cli.mjs +20 -2
- package/src/commands/doctor.mjs +41 -1
- package/src/commands/init.mjs +45 -1
- package/src/commands/review-render.mjs +255 -0
- package/src/commands/security-scan.mjs +367 -52
- package/src/lib/config.mjs +120 -104
- package/src/lib/gitignore.mjs +5 -1
- package/src/lib/npm-registry.mjs +159 -0
- package/src/lib/review/manifest-schema.json +149 -0
- package/src/lib/review/manifest-validator.mjs +93 -0
- package/src/lib/review/scope-slug.mjs +68 -0
- package/src/lib/sc-heuristic.mjs +263 -0
- package/src/lib/sc-scanner.mjs +60 -11
- package/src/lib/sc-sync.mjs +98 -8
- package/src/lib/telemetry.mjs +20 -0
package/CLAUDE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Workflow toolkit codebase. Rules live in `.claude/rules/` (auto-loaded).
|
|
4
4
|
|
|
5
5
|
**v2.0 direction:** Context-First SDLC Governance Layer (5 pillars — see `.dw/core/PILLARS.md`)
|
|
6
|
-
**Current:** v1.3.
|
|
6
|
+
**Current:** v1.3.6 (released 2026-05-14) · ADR-0001 active · ADR-0005 + ADR-0006 Accepted (Supply-Chain Guard 3-pillar AI-Native; sunset review 2026-08-12 per pillar) · v1.4 cuts pending telemetry
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> An AI development workflow toolkit for teams using agentic IDEs (Claude Code, Cursor) — from idea to review-ready commits.
|
|
4
4
|
|
|
5
|
-
**v1.3.
|
|
5
|
+
**v1.3.6** · `npm install -g dw-kit` · [Docs](docs/README.md) · [Get started](docs/get-started.md) · [Cheatsheet](docs/cheatsheet.md) · [Migration v1.3](MIGRATION-v1.3.md) · [Changelog](CHANGELOG.md)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -36,7 +36,9 @@ It’s designed for collaboration (Dev / Tech Lead / QA / PM) and keeps work aud
|
|
|
36
36
|
|
|
37
37
|
## Release notes
|
|
38
38
|
|
|
39
|
-
- **v1.
|
|
39
|
+
- **v1.4 (in progress)** — Optional **Review Render Pipeline** ([ADR-0007](.dw/decisions/0007-decoupled-review-render-pipeline.md)): `/dw:review --visual` plus a separate `dw-kit-render` package turn findings into SVG + PNG cards for PR comments / Slack / stakeholders. Pure JS + WASM, universal `npm install`, no system deps. See [`docs/review-renderer.md`](docs/review-renderer.md).
|
|
40
|
+
- **v1.3.6** (2026-05-14) — Supply-Chain Guard upgraded to 3-pillar architecture: OSV snapshot + curated IoC fixture (version-aware, wired into default scan) + **AI-Native NEW-package heuristic** that catches zero-day-ish risk at the AI-edit boundary. See [`CHANGELOG.md#v136--2026-05-14`](CHANGELOG.md#v136--2026-05-14) and [ADR-0006](.dw/decisions/0006-supply-chain-guard-heuristic.md).
|
|
41
|
+
- v1.3.5 (2026-05-12) — AI-Native Supply-Chain Guard: `dw security-scan` CLI + OSV.dev auto-sync + Edit-lockfile hook + scoped `.gitignore` for end-user projects. See [ADR-0005](.dw/decisions/0005-supply-chain-guard.md). Public 90-day sunset review committed for 2026-08-12.
|
|
40
42
|
- v1.3.4 (2026-04-21) — `/dw:plan` Quick Debate (red/blue self-critique), depth-driven activation
|
|
41
43
|
- v1.3.3 (2026-04-21) — Writer skills v1/v2 compatibility fix
|
|
42
44
|
- v1.3.0 (2026-04-21) — 5-pillar governance layer + telemetry foundation + ADRs + v2 task docs ([ADR-0001](.dw/decisions/0001-v2-pragmatic-lean.md))
|
|
@@ -44,6 +46,17 @@ It’s designed for collaboration (Dev / Tech Lead / QA / PM) and keeps work aud
|
|
|
44
46
|
- Full changelog: `CHANGELOG.md`
|
|
45
47
|
- Latest release notes: [GitHub Releases](https://github.com/dv-workflow/dv-workflow/releases)
|
|
46
48
|
|
|
49
|
+
### What's in v1.3.6 for your team
|
|
50
|
+
|
|
51
|
+
Reaction time when a supply-chain incident drops goes from 24-72 hours (wait for OSV index + npm publish cycle) to **~1 hour** (AI edits lockfile → hook fires → heuristic flags BEFORE anyone knows).
|
|
52
|
+
|
|
53
|
+
- **3-pillar default scan** — `dw security-scan` now runs OSV snapshot + curated IoC fixture + AI-Native heuristic in one go. Heuristic only probes NEW/bumped packages from `git show HEAD:package-lock.json` diff — typical edit = 1-5 packages probed, not 1000+.
|
|
54
|
+
- **npm registry metadata heuristic** — composite scoring on `recent_publish` (<72h), `popular_package` (≥10k weekly DL), `maintainer_change_recent`, `major_version_jump`, `typo_squat`. Per-package metadata cached 1h. Tunable threshold via `.dw/config/dw.config.yml`.
|
|
55
|
+
- **Version-aware IoC fixture** — `affected_range` per entry. Concrete versions out-of-range are skipped (no false positives). Range specs (`^1.169.0`) emit ambiguity warnings with severity downgrade.
|
|
56
|
+
- **Hook fires `dw security-scan --heuristic-only`** on Claude Code lockfile edit — fast diff-only check.
|
|
57
|
+
- **Telemetry per pillar** — `source: 'osv' | 'fixture' | 'heuristic'` tracked separately so the 2026-08-12 sunset review attributes catches to the right pillar.
|
|
58
|
+
- **`>1000 packages`** crash bug from v1.3.5 fixed (chunked OSV batches).
|
|
59
|
+
|
|
47
60
|
### What's in v1.3.5 for your team
|
|
48
61
|
|
|
49
62
|
- **`dw security-scan`** — scan for known supply-chain advisories against your project's `package-lock.json` (full match) or `package.json` (pre-install approximate). Uses [OSV.dev](https://osv.dev/) as data source (multi-maintainer upstream feed; no solo-curated bundle to go stale).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dw-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "AI development workflow toolkit — structured, quality-assured, team-ready. From requirements to dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"test": "node src/smoke-test.mjs",
|
|
59
|
+
"test:renderer": "cd packages/dw-kit-render && npm test",
|
|
59
60
|
"link": "npm link",
|
|
60
61
|
"test:e2e-local": "bash scripts/e2e-local-check.sh"
|
|
61
62
|
},
|
package/src/cli.mjs
CHANGED
|
@@ -103,11 +103,14 @@ export function run(argv) {
|
|
|
103
103
|
|
|
104
104
|
program
|
|
105
105
|
.command('security-scan')
|
|
106
|
-
.
|
|
106
|
+
.alias('scan')
|
|
107
|
+
.description('Scan project: 3-pillar supply-chain guard (OSV + fixture + AI-Native heuristic). Auto-syncs OSV snapshot if missing or stale.')
|
|
107
108
|
.option('--quick', 'Offline mode — use existing snapshot only (default behavior)')
|
|
108
109
|
.option('--update-db', 'Fetch fresh advisory snapshot from OSV.dev before scanning')
|
|
109
110
|
.option('--pre-install', 'Scan package.json without lockfile (OSV name-only + namespace fixture)')
|
|
110
|
-
.option('--offline', 'Skip network in --pre-install mode
|
|
111
|
+
.option('--offline', 'Skip network in --pre-install mode + skip pillar 3 heuristic')
|
|
112
|
+
.option('--no-heuristic', 'Skip pillar 3 (AI-Native NEW-package heuristic). Default: enabled on lockfile diff.')
|
|
113
|
+
.option('--heuristic-only', 'Run ONLY pillar 3 (used by hook). Skips OSV + fixture pillars.')
|
|
111
114
|
.option('--json', 'Output machine-readable JSON')
|
|
112
115
|
.option('--install-hook', 'Wire supply-chain-scan.sh into .claude/settings.json PostToolUse (idempotent)')
|
|
113
116
|
.option('--uninstall-hook', 'Remove supply-chain-scan.sh entry from .claude/settings.json')
|
|
@@ -128,6 +131,21 @@ export function run(argv) {
|
|
|
128
131
|
await securityScanCommand(opts);
|
|
129
132
|
});
|
|
130
133
|
|
|
134
|
+
const reviewCmd = program
|
|
135
|
+
.command('review')
|
|
136
|
+
.description('Review subcommands (ADR-0007)');
|
|
137
|
+
|
|
138
|
+
reviewCmd
|
|
139
|
+
.command('render <manifest>')
|
|
140
|
+
.description('Render a /dw:review --visual manifest into SVG/PNG (requires dw-kit-render) or markdown summary fallback')
|
|
141
|
+
.option('-f, --format <kind>', 'Override output formats: svg | png | both', null)
|
|
142
|
+
.option('-s, --strategy <name>', 'Override strategy: auto | plugin | markdown-only', null)
|
|
143
|
+
.option('-q, --quiet', 'Suppress info logs (still exits non-zero on hard errors)')
|
|
144
|
+
.action(async (manifest, opts) => {
|
|
145
|
+
const { reviewRenderCommand } = await import('./commands/review-render.mjs');
|
|
146
|
+
await reviewRenderCommand(manifest, opts);
|
|
147
|
+
});
|
|
148
|
+
|
|
131
149
|
program
|
|
132
150
|
.command('claude-vn-fix')
|
|
133
151
|
.description('Patch Claude CLI to fix Vietnamese IME (local, with backup/restore)')
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { join, resolve } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { header, ok, warn, err, info, log } from '../lib/ui.mjs';
|
|
5
|
-
import { loadConfig, getToolkitVersions } from '../lib/config.mjs';
|
|
6
|
+
import { loadConfig, getToolkitVersions, getReviewRendererConfig } from '../lib/config.mjs';
|
|
6
7
|
import { detectPlatform, platformLabel } from '../lib/platform.mjs';
|
|
7
8
|
import { snapshotInfo } from '../lib/sc-sync.mjs';
|
|
8
9
|
|
|
9
10
|
const TOOLKIT_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
11
|
+
const RENDER_PACKAGE = 'dw-kit-render';
|
|
12
|
+
|
|
13
|
+
function tryResolveRenderer(projectDir) {
|
|
14
|
+
if (process.env.DW_REVIEW_NO_RENDERER === '1') return { ok: false, reason: 'disabled by DW_REVIEW_NO_RENDERER=1' };
|
|
15
|
+
const reqProject = createRequire(join(projectDir, 'package.json'));
|
|
16
|
+
for (const req of [reqProject, createRequire(import.meta.url)]) {
|
|
17
|
+
try {
|
|
18
|
+
const pkgPath = req.resolve(`${RENDER_PACKAGE}/package.json`);
|
|
19
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
20
|
+
return { ok: true, version: pkg.version };
|
|
21
|
+
} catch {
|
|
22
|
+
// try next
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { ok: false };
|
|
26
|
+
}
|
|
10
27
|
|
|
11
28
|
const CORE_FILES = [
|
|
12
29
|
'.dw/core/WORKFLOW.md',
|
|
@@ -151,6 +168,29 @@ export async function doctorCommand() {
|
|
|
151
168
|
}
|
|
152
169
|
}
|
|
153
170
|
|
|
171
|
+
info('Review Render Pipeline (ADR-0007, opt-in)');
|
|
172
|
+
const rendererCfg = existsSync(configPath) ? getReviewRendererConfig(loadConfig(configPath) || {}) : getReviewRendererConfig({});
|
|
173
|
+
log(` Strategy : ${rendererCfg.strategy}`);
|
|
174
|
+
log(` Formats : ${rendererCfg.formats.join(', ')}`);
|
|
175
|
+
log(` Theme/Font : ${rendererCfg.theme} / ${rendererCfg.font}`);
|
|
176
|
+
|
|
177
|
+
if (rendererCfg.strategy === 'markdown-only') {
|
|
178
|
+
log(' Renderer : skipped (strategy=markdown-only)');
|
|
179
|
+
} else {
|
|
180
|
+
const r = tryResolveRenderer(projectDir);
|
|
181
|
+
if (r.ok) {
|
|
182
|
+
ok(`Renderer : dw-kit-render v${r.version || '?'} resolvable`);
|
|
183
|
+
} else if (rendererCfg.strategy === 'plugin') {
|
|
184
|
+
err("Renderer : 'dw-kit-render' NOT installed but strategy='plugin'");
|
|
185
|
+
log(` Install: npm install -g dw-kit-render`);
|
|
186
|
+
issues++;
|
|
187
|
+
} else {
|
|
188
|
+
warn(`Renderer : 'dw-kit-render' not installed — /dw:review --visual will fall back to markdown`);
|
|
189
|
+
log(` Install (optional): npm install -g dw-kit-render`);
|
|
190
|
+
warnings++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
154
194
|
info('Supply-Chain Guard (ADR-0005, opt-in)');
|
|
155
195
|
const sc = snapshotInfo(projectDir);
|
|
156
196
|
if (!sc.exists) {
|
package/src/commands/init.mjs
CHANGED
|
@@ -193,7 +193,51 @@ async function maybeInstallSupplyChainHook(projectDir, presetKey) {
|
|
|
193
193
|
}
|
|
194
194
|
if (result.action === 'added') {
|
|
195
195
|
ok('Supply-chain guard hook wired (ADR-0005 — opt-in flag enabled)');
|
|
196
|
-
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// UX: offer one-time OSV snapshot sync so end-user doesn't need to know
|
|
199
|
+
// `--update-db` exists. Lazy auto-refresh in `dw scan` covers the case
|
|
200
|
+
// when user declines, but offering during init gets snapshot ready upfront.
|
|
201
|
+
await maybeBootstrapOsvSnapshot(projectDir);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function maybeBootstrapOsvSnapshot(projectDir) {
|
|
205
|
+
// Non-TTY (CI / scripted): default yes so the snapshot exists for next scan.
|
|
206
|
+
// TTY: prompt user, default yes.
|
|
207
|
+
let shouldSync = true;
|
|
208
|
+
if (process.stdin.isTTY && !process.env.DW_INIT_NO_PROMPT) {
|
|
209
|
+
try {
|
|
210
|
+
const { prompt } = await import('enquirer');
|
|
211
|
+
const answer = await prompt({
|
|
212
|
+
type: 'confirm',
|
|
213
|
+
name: 'syncNow',
|
|
214
|
+
message: 'Sync OSV advisory snapshot now? (recommended first-time; ~10-30s)',
|
|
215
|
+
initial: true,
|
|
216
|
+
});
|
|
217
|
+
shouldSync = !!answer.syncNow;
|
|
218
|
+
} catch {
|
|
219
|
+
shouldSync = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!shouldSync) {
|
|
223
|
+
log(' Skipped. First `dw scan` will auto-sync if needed.');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const { findLockfile } = await import('../lib/sc-scanner.mjs');
|
|
228
|
+
if (!findLockfile(projectDir)) {
|
|
229
|
+
log(' No lockfile yet — first `dw scan` will sync after `npm install` runs.');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
log(' Syncing OSV snapshot...');
|
|
233
|
+
try {
|
|
234
|
+
const { syncSnapshotForProject } = await import('../lib/sc-sync.mjs');
|
|
235
|
+
const start = Date.now();
|
|
236
|
+
const res = await syncSnapshotForProject(projectDir);
|
|
237
|
+
const partialNote = res.partial ? ` (PARTIAL ${res.chunks.failed}/${res.chunks.total})` : '';
|
|
238
|
+
ok(`OSV snapshot ready — ${res.advisoryCount} advisories for ${res.packageCount} packages (${Date.now() - start}ms)${partialNote}`);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
warn(`OSV sync skipped: ${e.message}. Will retry on first \`dw scan\`.`);
|
|
197
241
|
}
|
|
198
242
|
}
|
|
199
243
|
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve, isAbsolute } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { header, ok, info, log, warn, err } from '../lib/ui.mjs';
|
|
8
|
+
import { loadConfigWithLocal, getReviewRendererConfig } from '../lib/config.mjs';
|
|
9
|
+
import { readManifest } from '../lib/review/manifest-validator.mjs';
|
|
10
|
+
import { logEvent } from '../lib/telemetry.mjs';
|
|
11
|
+
|
|
12
|
+
const RENDER_PACKAGE = 'dw-kit-render';
|
|
13
|
+
const INSTALL_HINT = ` Install renderer with: ${chalk.cyan('npm install -g dw-kit-render')}\n Or run: ${chalk.cyan('dw doctor')} for environment check.`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `dw review render <manifest>` — invoked by /dw:review --visual after writing manifest.
|
|
17
|
+
* See ADR-0007 for architecture.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} manifestPath - path to manifest.json (relative or absolute)
|
|
20
|
+
* @param {{format?: string, strategy?: string, quiet?: boolean}} opts
|
|
21
|
+
*/
|
|
22
|
+
export async function reviewRenderCommand(manifestPath, opts = {}) {
|
|
23
|
+
const projectDir = process.cwd();
|
|
24
|
+
const absManifest = isAbsolute(manifestPath) ? manifestPath : resolve(projectDir, manifestPath);
|
|
25
|
+
const startedAt = performance.now();
|
|
26
|
+
|
|
27
|
+
if (!opts.quiet) header('dw review render');
|
|
28
|
+
|
|
29
|
+
// 1. Load + validate manifest.
|
|
30
|
+
const parseResult = readManifest(absManifest);
|
|
31
|
+
if (!parseResult.ok) {
|
|
32
|
+
err(`Manifest invalid: ${absManifest}`);
|
|
33
|
+
for (const e of parseResult.errors.slice(0, 10)) {
|
|
34
|
+
log(` ${chalk.dim(e.path || '/')} — ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
logEvent({ event: 'review_render', action: 'fail', fallback_reason: 'invalid-manifest' }, projectDir);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
const manifest = parseResult.manifest;
|
|
40
|
+
|
|
41
|
+
// 2. Resolve config + strategy.
|
|
42
|
+
const config = loadConfigWithLocal(join(projectDir, '.dw', 'config')) || {};
|
|
43
|
+
const rendererCfg = getReviewRendererConfig(config);
|
|
44
|
+
const strategy = opts.strategy || rendererCfg.strategy;
|
|
45
|
+
const formats = parseFormats(opts.format) || rendererCfg.formats;
|
|
46
|
+
|
|
47
|
+
// 3. Resolve output directory.
|
|
48
|
+
const outDir = resolveOutputDir(rendererCfg.output_dir, manifest, projectDir);
|
|
49
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
if (!opts.quiet) {
|
|
52
|
+
info('Render context');
|
|
53
|
+
log(` Manifest : ${absManifest}`);
|
|
54
|
+
log(` Scope : ${manifest.scope} (slug: ${manifest.scope_slug || '—'})`);
|
|
55
|
+
log(` Findings : ${manifest.findings.length}`);
|
|
56
|
+
log(` Strategy : ${strategy}`);
|
|
57
|
+
log(` Formats : ${formats.join(', ')}`);
|
|
58
|
+
log(` Output dir : ${outDir}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4. Resolve renderer (dw-kit-render package).
|
|
62
|
+
const rendererResolved = strategy === 'markdown-only' ? null : await tryResolveRenderer(projectDir);
|
|
63
|
+
const finalStrategy = strategy === 'markdown-only'
|
|
64
|
+
? 'markdown-only'
|
|
65
|
+
: (rendererResolved ? 'plugin' : (strategy === 'plugin' ? 'plugin-missing' : 'fallback-markdown'));
|
|
66
|
+
|
|
67
|
+
// Plugin strategy was requested but package is missing — fail loudly.
|
|
68
|
+
if (strategy === 'plugin' && !rendererResolved) {
|
|
69
|
+
err(`Strategy 'plugin' requested but '${RENDER_PACKAGE}' is not installed.`);
|
|
70
|
+
log(INSTALL_HINT);
|
|
71
|
+
logEvent({ event: 'review_render', action: 'fail', fallback_reason: 'no-renderer', strategy, formats }, projectDir);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let artifacts = { svg: [], png: [] };
|
|
76
|
+
let renderErrors = [];
|
|
77
|
+
|
|
78
|
+
if (rendererResolved) {
|
|
79
|
+
try {
|
|
80
|
+
if (!opts.quiet) info('Rendering with dw-kit-render');
|
|
81
|
+
const result = await rendererResolved.render({
|
|
82
|
+
manifest,
|
|
83
|
+
outDir,
|
|
84
|
+
formats,
|
|
85
|
+
theme: rendererCfg.theme,
|
|
86
|
+
font: rendererCfg.font,
|
|
87
|
+
});
|
|
88
|
+
artifacts = {
|
|
89
|
+
svg: Array.isArray(result?.svgPaths) ? result.svgPaths : [],
|
|
90
|
+
png: Array.isArray(result?.pngPaths) ? result.pngPaths : [],
|
|
91
|
+
};
|
|
92
|
+
} catch (e) {
|
|
93
|
+
renderErrors.push(e.message || String(e));
|
|
94
|
+
warn(`Renderer error: ${e.message || e}`);
|
|
95
|
+
warn('Falling back to markdown-only summary.');
|
|
96
|
+
}
|
|
97
|
+
} else if (strategy !== 'markdown-only') {
|
|
98
|
+
if (!opts.quiet) {
|
|
99
|
+
warn(`'${RENDER_PACKAGE}' not found — emitting markdown summary only.`);
|
|
100
|
+
log(INSTALL_HINT);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. Write summary.md (always — works without renderer too).
|
|
105
|
+
const summaryPath = join(outDir, 'summary.md');
|
|
106
|
+
writeFileSync(summaryPath, buildSummaryMarkdown(manifest, artifacts, { outDir, projectDir }), 'utf-8');
|
|
107
|
+
|
|
108
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
109
|
+
|
|
110
|
+
if (!opts.quiet) {
|
|
111
|
+
console.log();
|
|
112
|
+
info('Artifacts');
|
|
113
|
+
log(` Summary : ${summaryPath}`);
|
|
114
|
+
if (artifacts.svg.length) log(` SVG : ${artifacts.svg.length} file(s)`);
|
|
115
|
+
if (artifacts.png.length) log(` PNG : ${artifacts.png.length} file(s)`);
|
|
116
|
+
if (!artifacts.svg.length && !artifacts.png.length) log(` ${chalk.dim('(markdown only — install dw-kit-render for images)')}`);
|
|
117
|
+
console.log();
|
|
118
|
+
if (renderErrors.length) {
|
|
119
|
+
warn(`${renderErrors.length} render error(s) — see above`);
|
|
120
|
+
} else {
|
|
121
|
+
ok(`Done in ${durationMs}ms`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
logEvent({
|
|
126
|
+
event: 'review_render',
|
|
127
|
+
action: renderErrors.length ? 'partial' : 'success',
|
|
128
|
+
strategy: finalStrategy,
|
|
129
|
+
formats,
|
|
130
|
+
findings: manifest.findings.length,
|
|
131
|
+
duration_ms: durationMs,
|
|
132
|
+
fallback_reason: rendererResolved ? null : (strategy === 'markdown-only' ? 'config-markdown-only' : 'no-renderer'),
|
|
133
|
+
}, projectDir);
|
|
134
|
+
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseFormats(value) {
|
|
139
|
+
if (!value) return null;
|
|
140
|
+
if (value === 'both') return ['svg', 'png'];
|
|
141
|
+
if (value === 'svg' || value === 'png') return [value];
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveOutputDir(configDir, manifest, projectDir) {
|
|
146
|
+
const baseDir = isAbsolute(configDir) ? configDir : join(projectDir, configDir);
|
|
147
|
+
const slug = manifest.scope_slug || manifest.scope || 'review';
|
|
148
|
+
return join(baseDir, slug);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function tryResolveRenderer(projectDir) {
|
|
152
|
+
// Test-only escape hatch: tests use this to force the markdown fallback even
|
|
153
|
+
// when a local renderer is dev-linked from a sibling packages/ dir.
|
|
154
|
+
if (process.env.DW_REVIEW_NO_RENDERER === '1') return null;
|
|
155
|
+
|
|
156
|
+
// Resolution order: project node_modules → CLI's own node_modules (global -g case).
|
|
157
|
+
// We use createRequire to get a *resolution* path, then dynamic-import that URL
|
|
158
|
+
// so the renderer can be ESM-only (Phase 1 ships ESM only).
|
|
159
|
+
for (const anchor of [join(projectDir, 'package.json'), import.meta.url]) {
|
|
160
|
+
try {
|
|
161
|
+
const req = createRequire(anchor);
|
|
162
|
+
const entry = req.resolve(RENDER_PACKAGE);
|
|
163
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
164
|
+
const render = mod?.render || mod?.default?.render;
|
|
165
|
+
if (typeof render !== 'function') {
|
|
166
|
+
throw new Error("dw-kit-render module did not export a 'render' function");
|
|
167
|
+
}
|
|
168
|
+
return { render };
|
|
169
|
+
} catch {
|
|
170
|
+
// try next anchor
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildSummaryMarkdown(manifest, artifacts, { outDir, projectDir }) {
|
|
177
|
+
const lines = [];
|
|
178
|
+
const counts = countBySeverity(manifest.findings);
|
|
179
|
+
const slug = manifest.scope_slug || manifest.scope;
|
|
180
|
+
|
|
181
|
+
lines.push(`# Review summary — ${manifest.scope}`);
|
|
182
|
+
lines.push('');
|
|
183
|
+
lines.push(`- Generated: ${manifest.generated_at}`);
|
|
184
|
+
if (manifest.review_meta?.diff_base) lines.push(`- Diff base: ${manifest.review_meta.diff_base}`);
|
|
185
|
+
if (manifest.review_meta?.files_reviewed != null) lines.push(`- Files reviewed: ${manifest.review_meta.files_reviewed}`);
|
|
186
|
+
if (manifest.task_id) lines.push(`- Task: \`${manifest.task_id}\``);
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push(`**Severity counts:** critical=${counts.critical}, warning=${counts.warning}, suggestion=${counts.suggestion}`);
|
|
189
|
+
lines.push('');
|
|
190
|
+
|
|
191
|
+
if (!manifest.findings.length) {
|
|
192
|
+
lines.push('No findings.');
|
|
193
|
+
lines.push('');
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lines.push('## Findings');
|
|
198
|
+
lines.push('');
|
|
199
|
+
for (const f of manifest.findings) {
|
|
200
|
+
const loc = formatLocation(f.location);
|
|
201
|
+
lines.push(`### [${f.severity.toUpperCase()}] ${f.title}`);
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push(`- File: \`${loc}\``);
|
|
204
|
+
if (f.rule_ref) lines.push(`- Rule: ${f.rule_ref}`);
|
|
205
|
+
|
|
206
|
+
const svg = artifacts.svg.find((p) => p.endsWith(`finding-${f.id}.svg`));
|
|
207
|
+
const png = artifacts.png.find((p) => p.endsWith(`finding-${f.id}.png`));
|
|
208
|
+
if (svg) lines.push(`- SVG: \`${relativeTo(svg, outDir)}\``);
|
|
209
|
+
if (png) lines.push(`- PNG: \`${relativeTo(png, outDir)}\``);
|
|
210
|
+
lines.push('');
|
|
211
|
+
lines.push(f.body);
|
|
212
|
+
lines.push('');
|
|
213
|
+
if (f.code_snippet) {
|
|
214
|
+
const lang = f.language || '';
|
|
215
|
+
lines.push('```' + lang);
|
|
216
|
+
lines.push(f.code_snippet);
|
|
217
|
+
lines.push('```');
|
|
218
|
+
lines.push('');
|
|
219
|
+
}
|
|
220
|
+
if (f.fix) {
|
|
221
|
+
lines.push(`**Fix:** ${f.fix}`);
|
|
222
|
+
lines.push('');
|
|
223
|
+
}
|
|
224
|
+
lines.push('---');
|
|
225
|
+
lines.push('');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
lines.push(`*Manifest: \`${relativeTo(join(outDir, 'manifest.json'), projectDir)}\`*`);
|
|
229
|
+
lines.push('');
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function countBySeverity(findings) {
|
|
234
|
+
const out = { critical: 0, warning: 0, suggestion: 0 };
|
|
235
|
+
for (const f of findings) {
|
|
236
|
+
if (out[f.severity] != null) out[f.severity]++;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatLocation(loc) {
|
|
242
|
+
if (!loc) return '?';
|
|
243
|
+
if (loc.line_start && loc.line_end && loc.line_start !== loc.line_end) {
|
|
244
|
+
return `${loc.file}:${loc.line_start}-${loc.line_end}`;
|
|
245
|
+
}
|
|
246
|
+
if (loc.line_start) return `${loc.file}:${loc.line_start}`;
|
|
247
|
+
return loc.file;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function relativeTo(target, base) {
|
|
251
|
+
const t = target.replace(/\\/g, '/');
|
|
252
|
+
const b = base.replace(/\\/g, '/').replace(/\/$/, '');
|
|
253
|
+
if (t.startsWith(b + '/')) return t.slice(b.length + 1);
|
|
254
|
+
return t;
|
|
255
|
+
}
|