@haystackeditor/cli 0.15.17 → 0.15.19

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.
Files changed (46) hide show
  1. package/README.md +45 -6
  2. package/dist/assets/skills/map-your-system.md +138 -0
  3. package/dist/commands/ask.d.ts +14 -0
  4. package/dist/commands/ask.js +20 -0
  5. package/dist/commands/inbox.d.ts +65 -0
  6. package/dist/commands/inbox.js +137 -0
  7. package/dist/commands/mcp.js +88 -134
  8. package/dist/commands/pr.d.ts +37 -1
  9. package/dist/commands/pr.js +95 -77
  10. package/dist/commands/review.d.ts +24 -0
  11. package/dist/commands/review.js +193 -0
  12. package/dist/commands/schema-cmd.js +1 -1
  13. package/dist/commands/skills.js +4 -0
  14. package/dist/commands/traces.d.ts +48 -0
  15. package/dist/commands/traces.js +92 -0
  16. package/dist/commands/triage.d.ts +12 -0
  17. package/dist/commands/triage.js +90 -58
  18. package/dist/commands/webhooks.js +3 -6
  19. package/dist/index.js +79 -148
  20. package/dist/schema.d.ts +5 -2
  21. package/dist/schema.js +5 -2
  22. package/dist/utils/analysis-api.d.ts +40 -0
  23. package/dist/utils/analysis-api.js +108 -3
  24. package/dist/utils/haystack-api.d.ts +20 -0
  25. package/dist/utils/haystack-api.js +66 -0
  26. package/package.json +4 -4
  27. package/schemas/ask.v1.json +40 -0
  28. package/schemas/inbox.v1.json +27 -0
  29. package/schemas/pr.v2.json +54 -0
  30. package/schemas/traces.v1.json +53 -0
  31. package/schemas/triage.v2.json +64 -0
  32. package/dist/assets/skills/install-verification.md +0 -67
  33. package/dist/commands/verification.d.ts +0 -21
  34. package/dist/commands/verification.js +0 -291
  35. package/dist/verification/contract.d.ts +0 -240
  36. package/dist/verification/contract.js +0 -115
  37. package/dist/verification/init.d.ts +0 -10
  38. package/dist/verification/init.js +0 -243
  39. package/dist/verification/manifest.d.ts +0 -49
  40. package/dist/verification/manifest.js +0 -186
  41. package/dist/verification/runner.d.ts +0 -20
  42. package/dist/verification/runner.js +0 -102
  43. package/dist/verification/safety.d.ts +0 -10
  44. package/dist/verification/safety.js +0 -252
  45. package/dist/verification/validate.d.ts +0 -12
  46. package/dist/verification/validate.js +0 -219
@@ -1,186 +0,0 @@
1
- /**
2
- * Artifact manifest + review packet.
3
- *
4
- * The manifest is the standardized output of every verification run — it's
5
- * what turns messy command output into reviewable evidence. The review packet
6
- * is the same data rendered as markdown for humans (and for posting on PRs).
7
- *
8
- * Status vocabulary (distinct on purpose — "failed verification" is different
9
- * from "could not verify"):
10
- * verified scenario ran and passed
11
- * failed scenario ran and failed
12
- * inconclusive artifacts collected without a run (or mixed signal)
13
- * environment_not_ready preflight/setup failed before the scenario ran
14
- * unsafe_to_verify safety policy forbids running (real side effects)
15
- * missing_contract no valid .haystack/verification.yml
16
- */
17
- import { spawnSync } from 'node:child_process';
18
- import * as fs from 'node:fs/promises';
19
- import * as path from 'node:path';
20
- const EXTENSION_TYPES = {
21
- '.png': 'screenshot',
22
- '.jpg': 'screenshot',
23
- '.jpeg': 'screenshot',
24
- '.webp': 'screenshot',
25
- '.gif': 'screenshot',
26
- '.webm': 'video',
27
- '.mp4': 'video',
28
- '.mov': 'video',
29
- '.log': 'log',
30
- '.txt': 'log',
31
- '.json': 'json',
32
- '.html': 'html',
33
- '.zip': 'trace',
34
- };
35
- export function classifyArtifact(filePath) {
36
- return EXTENSION_TYPES[path.extname(filePath).toLowerCase()] ?? 'file';
37
- }
38
- export function getHeadSha(cwd) {
39
- const res = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf-8' });
40
- if (res.status !== 0)
41
- return null;
42
- return res.stdout.trim() || null;
43
- }
44
- /**
45
- * Walk the artifact directory (plus configured log paths) and build the
46
- * manifest's artifact list. Manifest/packet outputs from previous runs are
47
- * excluded so evidence doesn't nest.
48
- */
49
- export async function collectArtifacts(opts) {
50
- const entries = [];
51
- const seen = new Set();
52
- const sinceMs = opts.since ? opts.since.getTime() - 1000 : null; // 1s slack for coarse mtimes
53
- const addFile = async (absPath, description) => {
54
- const relPath = path.relative(opts.rootDir, absPath);
55
- if (seen.has(relPath))
56
- return;
57
- if (/(^|\/)(manifest[^/]*\.json|review-packet[^/]*\.md)$/.test(relPath))
58
- return;
59
- let stat;
60
- try {
61
- stat = await fs.stat(absPath);
62
- }
63
- catch {
64
- return;
65
- }
66
- if (!stat.isFile())
67
- return;
68
- if (sinceMs !== null && stat.mtimeMs < sinceMs)
69
- return;
70
- seen.add(relPath);
71
- entries.push({
72
- type: classifyArtifact(absPath),
73
- path: relPath,
74
- ...(description ? { description } : {}),
75
- size_bytes: stat.size,
76
- });
77
- };
78
- const walk = async (dir) => {
79
- let dirEntries;
80
- try {
81
- dirEntries = await fs.readdir(dir, { withFileTypes: true });
82
- }
83
- catch {
84
- return;
85
- }
86
- for (const entry of dirEntries) {
87
- const full = path.join(dir, entry.name);
88
- if (entry.isDirectory()) {
89
- await walk(full);
90
- }
91
- else if (entry.isFile()) {
92
- await addFile(full);
93
- }
94
- }
95
- };
96
- await walk(opts.artifactDir);
97
- for (const extra of opts.extraPaths ?? []) {
98
- await addFile(path.resolve(opts.rootDir, extra), 'configured log path');
99
- }
100
- entries.sort((a, b) => a.path.localeCompare(b.path));
101
- return entries;
102
- }
103
- /** Read scenario-declared gaps from `<artifact_dir>/gaps.txt` (one per line), if present. */
104
- export async function readGaps(artifactDir) {
105
- try {
106
- const raw = await fs.readFile(path.join(artifactDir, 'gaps.txt'), 'utf-8');
107
- return raw
108
- .split('\n')
109
- .map((line) => line.trim())
110
- .filter((line) => line.length > 0 && !line.startsWith('#'));
111
- }
112
- catch {
113
- return [];
114
- }
115
- }
116
- export async function writeManifest(manifest, artifactDir) {
117
- await fs.mkdir(artifactDir, { recursive: true });
118
- const manifestPath = path.join(artifactDir, `manifest-${manifest.scenario}.json`);
119
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
120
- return manifestPath;
121
- }
122
- const STATUS_HEADLINES = {
123
- verified: '✅ Verified — evidence looks strong.',
124
- failed: '❌ Blocked — verification failed.',
125
- inconclusive: '🟡 Needs human review — verification is incomplete.',
126
- environment_not_ready: '⚠️ Not ready — the environment could not be prepared, so nothing was verified.',
127
- unsafe_to_verify: '🛑 Unsafe — the safety policy allows real external side effects; verification was not run.',
128
- missing_contract: '⚠️ Not ready — repo verification contract is missing or broken.',
129
- };
130
- export function renderReviewPacket(manifest) {
131
- const lines = [];
132
- lines.push(`# Haystack Verification — \`${manifest.scenario}\``);
133
- lines.push('');
134
- lines.push(STATUS_HEADLINES[manifest.status]);
135
- lines.push('');
136
- lines.push(`- Commit: \`${manifest.commit_sha ?? 'unknown'}\``);
137
- lines.push(`- Started: ${manifest.started_at}`);
138
- lines.push(`- Completed: ${manifest.completed_at}`);
139
- lines.push('');
140
- if (manifest.commands_run.length > 0) {
141
- lines.push('## Commands run');
142
- lines.push('');
143
- lines.push('| Command | Status | Duration |');
144
- lines.push('|---------|--------|----------|');
145
- for (const cmd of manifest.commands_run) {
146
- const icon = cmd.status === 'passed' ? '✅' : cmd.status === 'timeout' ? '⏱ timeout' : '❌ failed';
147
- lines.push(`| \`${cmd.command.replace(/\|/g, '\\|')}\` | ${icon} | ${(cmd.duration_ms / 1000).toFixed(1)}s |`);
148
- }
149
- lines.push('');
150
- }
151
- lines.push('## Artifacts');
152
- lines.push('');
153
- if (manifest.artifacts.length === 0) {
154
- lines.push('_No artifacts were captured._');
155
- }
156
- else {
157
- for (const artifact of manifest.artifacts) {
158
- const desc = artifact.description ? ` — ${artifact.description}` : '';
159
- lines.push(`- \`${artifact.path}\` (${artifact.type})${desc}`);
160
- }
161
- }
162
- lines.push('');
163
- lines.push('## Gaps');
164
- lines.push('');
165
- if (manifest.gaps.length === 0) {
166
- lines.push('_No gaps declared by this scenario._');
167
- }
168
- else {
169
- for (const gap of manifest.gaps) {
170
- lines.push(`- ${gap}`);
171
- }
172
- }
173
- lines.push('');
174
- lines.push('## Safety');
175
- lines.push('');
176
- lines.push(`- Real email sent: ${manifest.safety.real_email_sent ? '⚠️ yes' : 'no'}`);
177
- lines.push(`- Real payment attempted: ${manifest.safety.real_payment_attempted ? '⚠️ yes' : 'no'}`);
178
- lines.push(`- Customer data accessed: ${manifest.safety.customer_data_accessed ? '⚠️ yes' : 'no'}`);
179
- lines.push('');
180
- return lines.join('\n');
181
- }
182
- export async function writeReviewPacket(manifest, artifactDir) {
183
- const packetPath = path.join(artifactDir, `review-packet-${manifest.scenario}.md`);
184
- await fs.writeFile(packetPath, renderReviewPacket(manifest), 'utf-8');
185
- return packetPath;
186
- }
@@ -1,20 +0,0 @@
1
- export type CommandStatus = 'passed' | 'failed' | 'timeout';
2
- export interface CommandRunRecord {
3
- command: string;
4
- status: CommandStatus;
5
- exit_code: number | null;
6
- duration_ms: number;
7
- /** Repo-relative path to the captured stdout+stderr log, when one was written. */
8
- log_path?: string;
9
- }
10
- export interface RunCommandOptions {
11
- cwd: string;
12
- /** Wall-clock cap; the process group is killed when exceeded. */
13
- timeoutMs: number;
14
- /** Absolute path to tee combined stdout+stderr into (created if missing). */
15
- logFile?: string;
16
- env?: Record<string, string>;
17
- /** Suppress streaming to the terminal (still captured to logFile). */
18
- quiet?: boolean;
19
- }
20
- export declare function runCommand(command: string, opts: RunCommandOptions): Promise<CommandRunRecord>;
@@ -1,102 +0,0 @@
1
- /**
2
- * Command runner for verification contract commands.
3
- *
4
- * Every contract command (setup, preflight, scenario, safety check) runs
5
- * through here so the evidence trail is uniform: output is streamed to the
6
- * terminal AND teed to a log file under the artifact dir, and the result
7
- * carries exit code + duration for the manifest's `commands_run` section.
8
- */
9
- import { spawn, spawnSync } from 'node:child_process';
10
- import { createWriteStream } from 'node:fs';
11
- import * as fs from 'node:fs/promises';
12
- import * as path from 'node:path';
13
- export async function runCommand(command, opts) {
14
- const startedAt = Date.now();
15
- let logStream;
16
- if (opts.logFile) {
17
- await fs.mkdir(path.dirname(opts.logFile), { recursive: true });
18
- logStream = createWriteStream(opts.logFile);
19
- logStream.write(`$ ${command}\n`);
20
- }
21
- const record = await new Promise((resolvePromise) => {
22
- // detached puts the child in its own process group so a timeout kill
23
- // reaches grandchildren (dev servers spawned by package-manager wrappers),
24
- // not just the shell.
25
- const child = spawn(command, {
26
- cwd: opts.cwd,
27
- shell: true,
28
- detached: process.platform !== 'win32',
29
- env: { ...process.env, ...opts.env, HAYSTACK_VERIFICATION: '1' },
30
- stdio: ['ignore', 'pipe', 'pipe'],
31
- });
32
- let timedOut = false;
33
- const timer = setTimeout(() => {
34
- timedOut = true;
35
- killTree(child.pid);
36
- }, opts.timeoutMs);
37
- const onChunk = (chunk, stderr) => {
38
- logStream?.write(chunk);
39
- if (!opts.quiet) {
40
- (stderr ? process.stderr : process.stdout).write(chunk);
41
- }
42
- };
43
- child.stdout?.on('data', (c) => onChunk(c, false));
44
- child.stderr?.on('data', (c) => onChunk(c, true));
45
- const finish = (exitCode) => {
46
- clearTimeout(timer);
47
- const status = timedOut ? 'timeout' : exitCode === 0 ? 'passed' : 'failed';
48
- if (timedOut) {
49
- logStream?.write(`\n[haystack] command timed out after ${opts.timeoutMs}ms and was killed\n`);
50
- }
51
- resolvePromise({
52
- command,
53
- status,
54
- exit_code: exitCode,
55
- duration_ms: Date.now() - startedAt,
56
- });
57
- };
58
- child.on('error', (err) => {
59
- logStream?.write(`\n[haystack] failed to spawn: ${err.message}\n`);
60
- finish(null);
61
- });
62
- child.on('close', (code) => finish(code));
63
- });
64
- if (logStream) {
65
- await new Promise((res) => logStream.end(res));
66
- record.log_path = path.relative(opts.cwd, opts.logFile);
67
- }
68
- return record;
69
- }
70
- function killTree(pid) {
71
- if (pid === undefined)
72
- return;
73
- try {
74
- if (process.platform === 'win32') {
75
- const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
76
- windowsHide: true,
77
- encoding: 'utf8',
78
- });
79
- if (result.error) {
80
- throw result.error;
81
- }
82
- if (result.status !== 0) {
83
- const detail = result.stderr.trim() || result.stdout.trim() || `taskkill exited with code ${result.status}`;
84
- if (/not found|no running instance/i.test(detail)) {
85
- return;
86
- }
87
- throw new Error(detail);
88
- }
89
- }
90
- else {
91
- // Negative pid → the whole process group (see `detached` above).
92
- process.kill(-pid, 'SIGKILL');
93
- }
94
- }
95
- catch (error) {
96
- if (error.code === 'ESRCH') {
97
- return;
98
- }
99
- const message = error instanceof Error ? error.message : String(error);
100
- process.stderr.write(`\n[haystack] failed to kill process tree for pid ${pid}: ${message}\n`);
101
- }
102
- }
@@ -1,10 +0,0 @@
1
- export interface SafetyCheck {
2
- name: string;
3
- status: 'pass' | 'warn' | 'fail';
4
- detail: string;
5
- }
6
- export interface SafetyReport {
7
- ok: boolean;
8
- checks: SafetyCheck[];
9
- }
10
- export declare function checkSafety(rootDir?: string): Promise<SafetyReport>;
@@ -1,252 +0,0 @@
1
- /**
2
- * `haystack verification check-safety` — verifies the verification setup
3
- * itself can't hurt anyone:
4
- *
5
- * 1. The contract's safety policy forbids real side effects.
6
- * 2. The repo's production-disabled check (if configured) actually passes.
7
- * 3. Dev-only helper routes (/__dev/, log-me-in, dev-login) are gated
8
- * behind an environment check.
9
- * 4. Neither the contract nor collected artifacts contain secrets.
10
- *
11
- * Checks are heuristic by design (static scans, not proofs) — the strongest
12
- * guarantee remains the repo-owned `safety.production_disabled_check` command,
13
- * which should be a real test asserting dev routes 404 in production mode.
14
- */
15
- import * as fs from 'node:fs/promises';
16
- import * as path from 'node:path';
17
- import fg from 'fast-glob';
18
- import { loadContract } from './contract.js';
19
- import { runCommand } from './runner.js';
20
- const SOURCE_GLOBS = ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,rb,go,php}'];
21
- const SCAN_IGNORE = [
22
- '**/node_modules/**',
23
- '**/dist/**',
24
- '**/build/**',
25
- '**/.next/**',
26
- '**/vendor/**',
27
- '**/.git/**',
28
- '**/*.test.*',
29
- '**/*.spec.*',
30
- ];
31
- const MAX_SCAN_FILES = 5000;
32
- const MAX_FILE_BYTES = 512 * 1024;
33
- /** Signatures of dev-only helpers that must never be reachable in production. */
34
- const DEV_HELPER_PATTERNS = [
35
- /\/__dev\//,
36
- /log[-_]?me[-_]?in/i,
37
- /dev[-_]login/i,
38
- ];
39
- /** Evidence that a file gates behavior on the runtime environment. */
40
- const ENV_GUARD_PATTERNS = [
41
- /NODE_ENV/,
42
- /import\.meta\.env\.(DEV|PROD|MODE)/,
43
- /RAILS_ENV/,
44
- /DJANGO_SETTINGS|settings\.DEBUG/,
45
- /APP_ENV|FLASK_ENV|ENVIRONMENT/,
46
- ];
47
- const SECRET_PATTERNS = [
48
- { name: 'AWS access key', regex: /AKIA[0-9A-Z]{16}/ },
49
- { name: 'GitHub token', regex: /gh[pousr]_[A-Za-z0-9]{36,}/ },
50
- { name: 'GitHub fine-grained token', regex: /github_pat_[A-Za-z0-9_]{20,}/ },
51
- { name: 'OpenAI-style key', regex: /sk-[A-Za-z0-9_-]{24,}/ },
52
- { name: 'Anthropic key', regex: /sk-ant-[A-Za-z0-9_-]{24,}/ },
53
- { name: 'Haystack CLI token', regex: /hsk_live_[A-Za-z0-9]{16,}/ },
54
- { name: 'Slack token', regex: /xox[baprs]-[A-Za-z0-9-]{10,}/ },
55
- { name: 'Private key block', regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
56
- ];
57
- export async function checkSafety(rootDir = process.cwd()) {
58
- const checks = [];
59
- const loaded = await loadContract(rootDir);
60
- if (loaded.status !== 'ok') {
61
- checks.push({
62
- name: 'contract',
63
- status: 'fail',
64
- detail: loaded.status === 'missing'
65
- ? 'No verification contract — run `haystack verification init` first'
66
- : `Contract is invalid: ${loaded.errors[0]}`,
67
- });
68
- return { ok: false, checks };
69
- }
70
- const contract = loaded.contract;
71
- checkPolicy(contract, checks);
72
- await runProductionDisabledCheck(contract, rootDir, checks);
73
- await scanDevHelpers(rootDir, contract, checks);
74
- await scanSecrets(rootDir, loaded.path, contract, checks);
75
- return { ok: !checks.some((c) => c.status === 'fail'), checks };
76
- }
77
- function checkPolicy(contract, checks) {
78
- const { safety } = contract;
79
- const allowed = [
80
- ['allow_real_email', safety.allow_real_email],
81
- ['allow_real_payments', safety.allow_real_payments],
82
- ['allow_customer_data', safety.allow_customer_data],
83
- ].filter(([, v]) => v);
84
- if (allowed.length > 0) {
85
- for (const [flag] of allowed) {
86
- checks.push({
87
- name: 'safety-policy',
88
- status: 'fail',
89
- detail: `safety.${flag} is true — set it to false and use sandboxed/mocked services instead`,
90
- });
91
- }
92
- }
93
- else {
94
- checks.push({
95
- name: 'safety-policy',
96
- status: 'pass',
97
- detail: 'Policy forbids real email, real payments, and customer data',
98
- });
99
- }
100
- }
101
- async function runProductionDisabledCheck(contract, rootDir, checks) {
102
- const command = contract.safety.production_disabled_check;
103
- if (!command) {
104
- checks.push({
105
- name: 'production-disabled-check',
106
- status: 'warn',
107
- detail: 'No safety.production_disabled_check configured — add a test proving dev-only helpers are disabled in production builds',
108
- });
109
- return;
110
- }
111
- const logFile = path.resolve(rootDir, contract.environment.artifact_dir, 'logs', 'production-disabled-check.log');
112
- const result = await runCommand(command, {
113
- cwd: rootDir,
114
- timeoutMs: 5 * 60 * 1000,
115
- logFile,
116
- quiet: true,
117
- });
118
- checks.push({
119
- name: 'production-disabled-check',
120
- status: result.status === 'passed' ? 'pass' : 'fail',
121
- detail: result.status === 'passed'
122
- ? `\`${command}\` passed (${(result.duration_ms / 1000).toFixed(1)}s)`
123
- : `\`${command}\` ${result.status} (exit ${result.exit_code ?? 'n/a'}) — see ${result.log_path}`,
124
- });
125
- }
126
- async function scanDevHelpers(rootDir, contract, checks) {
127
- const files = await fg(SOURCE_GLOBS, {
128
- cwd: rootDir,
129
- ignore: [...SCAN_IGNORE, path.posix.join(contract.environment.artifact_dir, '**')],
130
- dot: false,
131
- onlyFiles: true,
132
- suppressErrors: true,
133
- });
134
- const ungated = [];
135
- let gatedCount = 0;
136
- let skippedCount = 0;
137
- for (const file of files.slice(0, MAX_SCAN_FILES)) {
138
- const full = path.join(rootDir, file);
139
- let content;
140
- try {
141
- const stat = await fs.stat(full);
142
- if (stat.size > MAX_FILE_BYTES) {
143
- skippedCount++;
144
- continue;
145
- }
146
- content = await fs.readFile(full, 'utf-8');
147
- }
148
- catch {
149
- skippedCount++;
150
- continue;
151
- }
152
- if (!DEV_HELPER_PATTERNS.some((p) => p.test(content)))
153
- continue;
154
- if (ENV_GUARD_PATTERNS.some((p) => p.test(content))) {
155
- gatedCount++;
156
- }
157
- else {
158
- ungated.push(file);
159
- }
160
- }
161
- if (ungated.length > 0) {
162
- checks.push({
163
- name: 'dev-helper-gating',
164
- status: 'fail',
165
- detail: `Dev-only helper code with no environment guard in: ${ungated.slice(0, 5).join(', ')}${ungated.length > 5 ? ` (+${ungated.length - 5} more)` : ''} — gate on NODE_ENV (or equivalent) so it cannot run in production`,
166
- });
167
- }
168
- else {
169
- checks.push({
170
- name: 'dev-helper-gating',
171
- status: 'pass',
172
- detail: gatedCount > 0
173
- ? `${gatedCount} file(s) with dev helpers all reference an environment guard`
174
- : 'No dev-only helper routes detected',
175
- });
176
- }
177
- if (files.length > MAX_SCAN_FILES) {
178
- checks.push({
179
- name: 'dev-helper-gating',
180
- status: 'warn',
181
- detail: `Scanned the first ${MAX_SCAN_FILES} of ${files.length} source files — coverage was truncated`,
182
- });
183
- }
184
- if (skippedCount > 0) {
185
- // Reduced coverage must be visible — silence reads as "covered".
186
- checks.push({
187
- name: 'dev-helper-gating',
188
- status: 'warn',
189
- detail: `${skippedCount} source file(s) were skipped (unreadable or >${MAX_FILE_BYTES / 1024}KB) and not scanned`,
190
- });
191
- }
192
- }
193
- async function scanSecrets(rootDir, contractPath, contract, checks) {
194
- const targets = [contractPath];
195
- const artifactDir = path.resolve(rootDir, contract.environment.artifact_dir);
196
- const artifactFiles = await fg('**/*', {
197
- cwd: artifactDir,
198
- onlyFiles: true,
199
- dot: true,
200
- suppressErrors: true,
201
- });
202
- for (const file of artifactFiles)
203
- targets.push(path.join(artifactDir, file));
204
- const findings = [];
205
- let skippedCount = 0;
206
- for (const target of targets) {
207
- let content;
208
- try {
209
- const stat = await fs.stat(target);
210
- if (stat.size > 1024 * 1024) {
211
- skippedCount++; // binary/huge artifacts — not scanned
212
- continue;
213
- }
214
- content = await fs.readFile(target, 'utf-8');
215
- }
216
- catch {
217
- skippedCount++;
218
- continue;
219
- }
220
- for (const { name, regex } of SECRET_PATTERNS) {
221
- const match = content.match(regex);
222
- if (match) {
223
- findings.push(`${name} in ${path.relative(rootDir, target)} (${redact(match[0])})`);
224
- }
225
- }
226
- }
227
- if (findings.length > 0) {
228
- checks.push({
229
- name: 'secret-scan',
230
- status: 'fail',
231
- detail: `Possible secrets found: ${findings.join('; ')} — redact logs/artifacts and rotate anything real`,
232
- });
233
- }
234
- else {
235
- checks.push({
236
- name: 'secret-scan',
237
- status: 'pass',
238
- detail: `No secrets detected in the contract or ${artifactFiles.length - skippedCount} artifact file(s)`,
239
- });
240
- }
241
- if (skippedCount > 0) {
242
- // Reduced coverage must be visible — silence reads as "covered".
243
- checks.push({
244
- name: 'secret-scan',
245
- status: 'warn',
246
- detail: `${skippedCount} file(s) were skipped (unreadable or >1MB) and not scanned for secrets`,
247
- });
248
- }
249
- }
250
- function redact(secret) {
251
- return `${secret.slice(0, 8)}…`;
252
- }
@@ -1,12 +0,0 @@
1
- export type Readiness = 'ready' | 'partially_ready' | 'not_ready' | 'unsafe';
2
- export type CheckLevel = 'pass' | 'warn' | 'fail' | 'unsafe';
3
- export interface ValidationItem {
4
- level: CheckLevel;
5
- message: string;
6
- }
7
- export interface ValidationReport {
8
- readiness: Readiness;
9
- contract_path: string;
10
- items: ValidationItem[];
11
- }
12
- export declare function validateContract(rootDir?: string): Promise<ValidationReport>;