@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.
- package/README.md +45 -6
- package/dist/assets/skills/map-your-system.md +138 -0
- package/dist/commands/ask.d.ts +14 -0
- package/dist/commands/ask.js +20 -0
- package/dist/commands/inbox.d.ts +65 -0
- package/dist/commands/inbox.js +137 -0
- package/dist/commands/mcp.js +88 -134
- package/dist/commands/pr.d.ts +37 -1
- package/dist/commands/pr.js +95 -77
- package/dist/commands/review.d.ts +24 -0
- package/dist/commands/review.js +193 -0
- package/dist/commands/schema-cmd.js +1 -1
- package/dist/commands/skills.js +4 -0
- package/dist/commands/traces.d.ts +48 -0
- package/dist/commands/traces.js +92 -0
- package/dist/commands/triage.d.ts +12 -0
- package/dist/commands/triage.js +90 -58
- package/dist/commands/webhooks.js +3 -6
- package/dist/index.js +79 -148
- package/dist/schema.d.ts +5 -2
- package/dist/schema.js +5 -2
- package/dist/utils/analysis-api.d.ts +40 -0
- package/dist/utils/analysis-api.js +108 -3
- package/dist/utils/haystack-api.d.ts +20 -0
- package/dist/utils/haystack-api.js +66 -0
- package/package.json +4 -4
- package/schemas/ask.v1.json +40 -0
- package/schemas/inbox.v1.json +27 -0
- package/schemas/pr.v2.json +54 -0
- package/schemas/traces.v1.json +53 -0
- package/schemas/triage.v2.json +64 -0
- package/dist/assets/skills/install-verification.md +0 -67
- package/dist/commands/verification.d.ts +0 -21
- package/dist/commands/verification.js +0 -291
- package/dist/verification/contract.d.ts +0 -240
- package/dist/verification/contract.js +0 -115
- package/dist/verification/init.d.ts +0 -10
- package/dist/verification/init.js +0 -243
- package/dist/verification/manifest.d.ts +0 -49
- package/dist/verification/manifest.js +0 -186
- package/dist/verification/runner.d.ts +0 -20
- package/dist/verification/runner.js +0 -102
- package/dist/verification/safety.d.ts +0 -10
- package/dist/verification/safety.js +0 -252
- package/dist/verification/validate.d.ts +0 -12
- package/dist/verification/validate.js +0 -219
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract validator — answers "can Haystack trust this repo's verification
|
|
3
|
-
* setup?" without running anything expensive.
|
|
4
|
-
*
|
|
5
|
-
* Readiness verdicts:
|
|
6
|
-
* ready all checks pass
|
|
7
|
-
* partially_ready usable, but with gaps (warnings)
|
|
8
|
-
* not_ready contract missing/invalid or references broken commands
|
|
9
|
-
* unsafe the contract permits real external side effects, or
|
|
10
|
-
* defines login helpers with no production-disabled proof
|
|
11
|
-
*/
|
|
12
|
-
import * as fs from 'node:fs/promises';
|
|
13
|
-
import * as path from 'node:path';
|
|
14
|
-
import { loadContract } from './contract.js';
|
|
15
|
-
/** Package-manager subcommands that are built-ins, not package.json scripts. */
|
|
16
|
-
const PM_BUILTINS = new Set([
|
|
17
|
-
'install', 'i', 'ci', 'add', 'remove', 'rm', 'uninstall', 'update', 'up', 'upgrade',
|
|
18
|
-
'exec', 'dlx', 'npx', 'create', 'init', 'link', 'unlink', 'publish', 'pack',
|
|
19
|
-
'audit', 'outdated', 'why', 'list', 'ls', 'prune', 'rebuild', 'config', 'store',
|
|
20
|
-
]);
|
|
21
|
-
export async function validateContract(rootDir = process.cwd()) {
|
|
22
|
-
const loaded = await loadContract(rootDir);
|
|
23
|
-
if (loaded.status === 'missing') {
|
|
24
|
-
return {
|
|
25
|
-
readiness: 'not_ready',
|
|
26
|
-
contract_path: loaded.path,
|
|
27
|
-
items: [
|
|
28
|
-
{
|
|
29
|
-
level: 'fail',
|
|
30
|
-
message: `No contract found at ${path.relative(rootDir, loaded.path)} — run \`haystack verification init\``,
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
if (loaded.status === 'invalid') {
|
|
36
|
-
return {
|
|
37
|
-
readiness: 'not_ready',
|
|
38
|
-
contract_path: loaded.path,
|
|
39
|
-
items: loaded.errors.map((error) => ({ level: 'fail', message: error })),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
const contract = loaded.contract;
|
|
43
|
-
const items = [];
|
|
44
|
-
items.push({ level: 'pass', message: 'Contract parses and matches the schema' });
|
|
45
|
-
// ── Command references ─────────────────────────────────────────────────────
|
|
46
|
-
const packageScripts = await readPackageScripts(rootDir);
|
|
47
|
-
const commands = [];
|
|
48
|
-
const env = contract.environment;
|
|
49
|
-
if (env.setup)
|
|
50
|
-
commands.push({ label: 'environment.setup', command: env.setup });
|
|
51
|
-
if (env.start)
|
|
52
|
-
commands.push({ label: 'environment.start', command: env.start });
|
|
53
|
-
commands.push({ label: 'environment.preflight', command: env.preflight });
|
|
54
|
-
if (env.reset)
|
|
55
|
-
commands.push({ label: 'environment.reset', command: env.reset });
|
|
56
|
-
if (contract.safety.production_disabled_check) {
|
|
57
|
-
commands.push({
|
|
58
|
-
label: 'safety.production_disabled_check',
|
|
59
|
-
command: contract.safety.production_disabled_check,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
for (const [name, scenario] of Object.entries(contract.scenarios)) {
|
|
63
|
-
commands.push({ label: `scenarios.${name}`, command: scenario.command });
|
|
64
|
-
}
|
|
65
|
-
for (const [name, persona] of Object.entries(contract.personas)) {
|
|
66
|
-
commands.push({ label: `personas.${name}.login`, command: persona.login });
|
|
67
|
-
}
|
|
68
|
-
for (const { label, command } of commands) {
|
|
69
|
-
const issue = await checkCommandReference(command, rootDir, packageScripts);
|
|
70
|
-
if (issue) {
|
|
71
|
-
items.push({ level: issue.level, message: `${label}: ${issue.message}` });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// ── Coverage warnings ──────────────────────────────────────────────────────
|
|
75
|
-
const scenarioNames = Object.keys(contract.scenarios);
|
|
76
|
-
items.push({
|
|
77
|
-
level: 'pass',
|
|
78
|
-
message: `${scenarioNames.length} scenario${scenarioNames.length === 1 ? '' : 's'} defined (${scenarioNames.join(', ')})`,
|
|
79
|
-
});
|
|
80
|
-
if (!scenarioNames.includes('smoke')) {
|
|
81
|
-
items.push({
|
|
82
|
-
level: 'warn',
|
|
83
|
-
message: 'No `smoke` scenario — add a basic logged-in smoke test so every risky PR has a baseline check',
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
if (Object.keys(contract.personas).length === 0) {
|
|
87
|
-
items.push({
|
|
88
|
-
level: 'warn',
|
|
89
|
-
message: 'No personas defined — logged-in and role-based flows cannot be verified',
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
if (!env.start) {
|
|
93
|
-
items.push({
|
|
94
|
-
level: 'warn',
|
|
95
|
-
message: 'No start command — scenarios must boot the app themselves',
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
if (!env.reset) {
|
|
99
|
-
items.push({
|
|
100
|
-
level: 'warn',
|
|
101
|
-
message: 'No reset command — repeated runs may not start from a clean state',
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
const artifactDir = path.resolve(rootDir, env.artifact_dir);
|
|
105
|
-
if (await pathExists(artifactDir)) {
|
|
106
|
-
items.push({ level: 'pass', message: `Artifact directory exists (${env.artifact_dir})` });
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
items.push({
|
|
110
|
-
level: 'pass',
|
|
111
|
-
message: `Artifact directory configured (${env.artifact_dir}) — will be created on first run`,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
// ── Safety ─────────────────────────────────────────────────────────────────
|
|
115
|
-
const safety = contract.safety;
|
|
116
|
-
for (const [flag, value] of [
|
|
117
|
-
['allow_real_email', safety.allow_real_email],
|
|
118
|
-
['allow_real_payments', safety.allow_real_payments],
|
|
119
|
-
['allow_customer_data', safety.allow_customer_data],
|
|
120
|
-
]) {
|
|
121
|
-
if (value) {
|
|
122
|
-
items.push({
|
|
123
|
-
level: 'unsafe',
|
|
124
|
-
message: `safety.${flag} is true — verification must not have real external side effects`,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (Object.keys(contract.personas).length > 0 && !safety.production_disabled_check) {
|
|
129
|
-
items.push({
|
|
130
|
-
level: 'unsafe',
|
|
131
|
-
message: 'Personas/login helpers are defined but safety.production_disabled_check is missing — there is no proof dev login is disabled in production builds',
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
if (safety.production_disabled_check) {
|
|
135
|
-
items.push({ level: 'pass', message: 'Production-disabled check configured' });
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
readiness: computeReadiness(items),
|
|
139
|
-
contract_path: loaded.path,
|
|
140
|
-
items,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
function computeReadiness(items) {
|
|
144
|
-
if (items.some((i) => i.level === 'unsafe'))
|
|
145
|
-
return 'unsafe';
|
|
146
|
-
if (items.some((i) => i.level === 'fail'))
|
|
147
|
-
return 'not_ready';
|
|
148
|
-
if (items.some((i) => i.level === 'warn'))
|
|
149
|
-
return 'partially_ready';
|
|
150
|
-
return 'ready';
|
|
151
|
-
}
|
|
152
|
-
async function readPackageScripts(rootDir) {
|
|
153
|
-
try {
|
|
154
|
-
const pkg = JSON.parse(await fs.readFile(path.join(rootDir, 'package.json'), 'utf-8'));
|
|
155
|
-
return pkg.scripts ?? {};
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
return null; // Not a Node project — script checks don't apply.
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Best-effort static check that a contract command points at something real.
|
|
163
|
-
* Only definitive breakage is a `fail`; ambiguous cases (bare `pnpm foo` that
|
|
164
|
-
* could resolve to a node_modules binary) are warnings.
|
|
165
|
-
*/
|
|
166
|
-
async function checkCommandReference(command, rootDir, packageScripts) {
|
|
167
|
-
const tokens = command.trim().split(/\s+/);
|
|
168
|
-
// `bash scripts/x`, `sh ./scripts/x`, `node scripts/x.mjs`, or a direct
|
|
169
|
-
// `./scripts/x` — the referenced file must exist.
|
|
170
|
-
let scriptPath = null;
|
|
171
|
-
if (['bash', 'sh', 'node', 'python', 'python3', 'ruby'].includes(tokens[0]) && tokens[1]) {
|
|
172
|
-
scriptPath = tokens[1];
|
|
173
|
-
}
|
|
174
|
-
else if (tokens[0]?.startsWith('./') || tokens[0]?.startsWith('scripts/')) {
|
|
175
|
-
scriptPath = tokens[0];
|
|
176
|
-
}
|
|
177
|
-
if (scriptPath && !scriptPath.startsWith('-')) {
|
|
178
|
-
if (!(await pathExists(path.resolve(rootDir, scriptPath)))) {
|
|
179
|
-
return { level: 'fail', message: `references ${scriptPath}, which does not exist` };
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
// Package-manager script references.
|
|
184
|
-
if (packageScripts && ['pnpm', 'npm', 'yarn', 'bun'].includes(tokens[0])) {
|
|
185
|
-
let scriptName;
|
|
186
|
-
let explicitRun = false;
|
|
187
|
-
if (tokens[1] === 'run' && tokens[2]) {
|
|
188
|
-
scriptName = tokens[2];
|
|
189
|
-
explicitRun = true;
|
|
190
|
-
}
|
|
191
|
-
else if (tokens[1] && !tokens[1].startsWith('-')) {
|
|
192
|
-
scriptName = tokens[1];
|
|
193
|
-
}
|
|
194
|
-
if (!scriptName || PM_BUILTINS.has(scriptName))
|
|
195
|
-
return null;
|
|
196
|
-
if (packageScripts[scriptName])
|
|
197
|
-
return null;
|
|
198
|
-
if (explicitRun) {
|
|
199
|
-
return { level: 'fail', message: `script "${scriptName}" is not in package.json scripts` };
|
|
200
|
-
}
|
|
201
|
-
// Bare `pnpm foo` can also resolve to node_modules/.bin/foo.
|
|
202
|
-
if (await pathExists(path.join(rootDir, 'node_modules', '.bin', scriptName)))
|
|
203
|
-
return null;
|
|
204
|
-
return {
|
|
205
|
-
level: 'warn',
|
|
206
|
-
message: `"${scriptName}" is not in package.json scripts (and node_modules/.bin/${scriptName} was not found)`,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
async function pathExists(p) {
|
|
212
|
-
try {
|
|
213
|
-
await fs.access(p);
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
}
|