@haystackeditor/cli 0.25.0 → 0.26.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 +39 -455
- package/dist/capture/adapters/django.js +7 -0
- package/dist/capture/adapters/index.js +10 -0
- package/dist/capture/adapters/next-build.js +113 -0
- package/dist/capture/adapters/next.js +272 -105
- package/dist/capture/adapters/nuxt.js +7 -0
- package/dist/capture/adapters/rails.js +7 -0
- package/dist/capture/adapters/react-router.js +7 -0
- package/dist/capture/adapters/sveltekit.js +7 -0
- package/dist/capture/adapters/vite.js +14 -0
- package/dist/capture/app-config.js +27 -2
- package/dist/capture/csp.js +7 -6
- package/dist/capture/url-rewrites.js +232 -0
- package/dist/capture-step.js +3 -0
- package/dist/commands/capture-brief.js +41 -32
- package/dist/commands/capture-manifest.js +25 -17
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-capture.js +44 -27
- package/dist/commands/init-telemetry.js +77 -14
- package/dist/commands/init.js +19 -4
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/verify-onboarding.js +16 -0
- package/dist/commands/verify.js +70 -15
- package/dist/index.js +29 -923
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +0 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/init.v1.json +1 -1
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adding one dependency to an app's lockfile without moving anything else (rule 13: init must never change what a build
|
|
3
|
+
* resolves beyond what it adds). Measured on real lockfiles (Rallly's pnpm workspace, and scratch workspaces whose app
|
|
4
|
+
* locks older versions of packages the CLI also uses):
|
|
5
|
+
* - yarn 1, yarn 4 and bun add only the new descriptors and entries; nothing else moves.
|
|
6
|
+
* - npm hoists the new tree and replaces hoisted packages the app already locks (ws, yaml, zod moved to the CLI's
|
|
7
|
+
* versions); with `--install-strategy=nested` it places the new tree under the dependency and moves nothing.
|
|
8
|
+
* - pnpm re-resolves the dependencies of every package the new tree shares with the old one to the newest version in the
|
|
9
|
+
* graph (47 entries in Rallly, from @smithy/* to express), and no flag stops it. So init lets pnpm resolve the new
|
|
10
|
+
* dependency (`--lockfile-only`), keeps every entry the lockfile had, adds the entries only the new dependency
|
|
11
|
+
* reaches (mergePnpmLockfile), and installs exactly that (`pnpm install --frozen-lockfile`, which also proves it).
|
|
12
|
+
* lockfileDrift checks the result for every format init can read, so a package manager that moves anything else fails the
|
|
13
|
+
* change (and init restores every file).
|
|
14
|
+
*/
|
|
15
|
+
function unquote(text) {
|
|
16
|
+
if (text.startsWith('\'') && text.endsWith('\''))
|
|
17
|
+
return text.slice(1, -1).replace(/''/g, '\'');
|
|
18
|
+
if (text.startsWith('"') && text.endsWith('"'))
|
|
19
|
+
return JSON.parse(text);
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
/** An entry's key line (` 'name@1.0.0(peer@2)':`, ` name@1.0.0: {}`) as its name. */
|
|
23
|
+
function keyOf(line) {
|
|
24
|
+
return unquote(line.trim().replace(/: \{\}$/, '').replace(/:$/, ''));
|
|
25
|
+
}
|
|
26
|
+
const ENTRY_SECTIONS = new Set(['importers', 'packages', 'snapshots']);
|
|
27
|
+
/** pnpm-lock.yaml as its top-level sections, the importers, packages and snapshots split into entries kept as written. */
|
|
28
|
+
function parseLock(text) {
|
|
29
|
+
const sections = [];
|
|
30
|
+
for (const line of text.replace(/\n$/, '').split('\n')) {
|
|
31
|
+
if (/^\S/.test(line))
|
|
32
|
+
sections.push({ header: line, entries: null, raw: [] });
|
|
33
|
+
else if (sections.length === 0)
|
|
34
|
+
throw new Error('pnpm-lock.yaml does not start with a section');
|
|
35
|
+
else
|
|
36
|
+
sections[sections.length - 1].raw.push(line);
|
|
37
|
+
}
|
|
38
|
+
for (const section of sections) {
|
|
39
|
+
if (!ENTRY_SECTIONS.has(section.header.replace(/:$/, '')))
|
|
40
|
+
continue;
|
|
41
|
+
const entries = [];
|
|
42
|
+
for (const line of section.raw) {
|
|
43
|
+
if (/^ {2}\S/.test(line))
|
|
44
|
+
entries.push({ name: keyOf(line), lines: [line] });
|
|
45
|
+
else if (line.trim() !== '') {
|
|
46
|
+
if (entries.length === 0)
|
|
47
|
+
throw new Error(`pnpm-lock.yaml's ${section.header} has a line outside any entry`);
|
|
48
|
+
entries[entries.length - 1].lines.push(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
section.entries = entries;
|
|
52
|
+
}
|
|
53
|
+
return sections;
|
|
54
|
+
}
|
|
55
|
+
function formatLock(sections) {
|
|
56
|
+
const blocks = sections.map(section => {
|
|
57
|
+
if (section.entries === null)
|
|
58
|
+
return [section.header, ...section.raw].join('\n');
|
|
59
|
+
return [section.header, '', section.entries.map(entry => entry.lines.join('\n')).join('\n\n'), ''].join('\n');
|
|
60
|
+
});
|
|
61
|
+
return `${blocks.join('\n').replace(/\n+$/, '')}\n`;
|
|
62
|
+
}
|
|
63
|
+
function section(sections, name) {
|
|
64
|
+
const found = sections.find(item => item.header === `${name}:`);
|
|
65
|
+
if (!found?.entries)
|
|
66
|
+
throw new Error(`pnpm-lock.yaml has no ${name} section`);
|
|
67
|
+
return found.entries;
|
|
68
|
+
}
|
|
69
|
+
/** A snapshot's edges: `dependencies` (required) and `optionalDependencies`, as snapshot keys. */
|
|
70
|
+
function edges(entry) {
|
|
71
|
+
const out = [];
|
|
72
|
+
let field = null;
|
|
73
|
+
for (const line of entry.lines.slice(1)) {
|
|
74
|
+
const header = /^ {4}(\w+):$/.exec(line);
|
|
75
|
+
if (header) {
|
|
76
|
+
field = header[1];
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (/^ {4}\S/.test(line)) {
|
|
80
|
+
field = null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const dependency = /^ {6}(.+?): (.+)$/.exec(line);
|
|
84
|
+
if (!dependency || (field !== 'dependencies' && field !== 'optionalDependencies'))
|
|
85
|
+
continue;
|
|
86
|
+
const name = unquote(dependency[1]);
|
|
87
|
+
const version = unquote(dependency[2]);
|
|
88
|
+
if (version.startsWith('link:'))
|
|
89
|
+
continue;
|
|
90
|
+
// An alias resolves to another package: its value is that package's own key.
|
|
91
|
+
const key = /^(@[^/@]+\/)?[^@(]+@/.test(version) && !/^\d/.test(version) ? version : `${name}@${version}`;
|
|
92
|
+
out.push({ key, optional: field === 'optionalDependencies' });
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** The snapshot key's package (`name@1.0.0(peer@2)` → `name@1.0.0`). */
|
|
97
|
+
function packageKey(key) {
|
|
98
|
+
const at = key.indexOf('@', 1);
|
|
99
|
+
const peers = key.indexOf('(', at);
|
|
100
|
+
return peers === -1 ? key : key.slice(0, peers);
|
|
101
|
+
}
|
|
102
|
+
/** An importer's dependencies by field (`dependencies:`, `devDependencies:`, …), each entry's lines as written. */
|
|
103
|
+
function importerFields(entry) {
|
|
104
|
+
const fields = [];
|
|
105
|
+
for (const line of entry.lines.slice(1)) {
|
|
106
|
+
const header = /^ {4}(\w+):$/.exec(line);
|
|
107
|
+
if (header) {
|
|
108
|
+
fields.push({ field: header[1], dependencies: [] });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const current = fields[fields.length - 1];
|
|
112
|
+
if (!current)
|
|
113
|
+
throw new Error(`pnpm-lock.yaml's importer ${entry.name} has a line outside any field`);
|
|
114
|
+
if (/^ {6}\S/.test(line))
|
|
115
|
+
current.dependencies.push({ field: current.field, name: keyOf(line.slice(4)), lines: [line] });
|
|
116
|
+
else if (current.dependencies.length > 0)
|
|
117
|
+
current.dependencies[current.dependencies.length - 1].lines.push(line);
|
|
118
|
+
else
|
|
119
|
+
throw new Error(`pnpm-lock.yaml's importer ${entry.name} has an unexpected ${current.field} line`);
|
|
120
|
+
}
|
|
121
|
+
return fields;
|
|
122
|
+
}
|
|
123
|
+
const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
124
|
+
function versionOf(dependency) {
|
|
125
|
+
const line = dependency.lines.find(item => /^ {8}version: /.test(item));
|
|
126
|
+
if (!line)
|
|
127
|
+
throw new Error(`pnpm-lock.yaml lists ${dependency.name} with no version`);
|
|
128
|
+
return unquote(line.trim().slice('version: '.length));
|
|
129
|
+
}
|
|
130
|
+
/** Every snapshot key reachable from `roots` (required edges first: `required` holds those reached without an optional edge). */
|
|
131
|
+
function reach(snapshots, roots) {
|
|
132
|
+
const all = new Set();
|
|
133
|
+
const required = new Set();
|
|
134
|
+
const queue = roots.map(root => ({ ...root }));
|
|
135
|
+
while (queue.length > 0) {
|
|
136
|
+
const { key, optional } = queue.shift();
|
|
137
|
+
if (optional ? all.has(key) : required.has(key))
|
|
138
|
+
continue;
|
|
139
|
+
const entry = snapshots.get(key);
|
|
140
|
+
if (!entry)
|
|
141
|
+
throw new Error(`pnpm-lock.yaml references ${key}, which it does not list`);
|
|
142
|
+
all.add(key);
|
|
143
|
+
if (!optional)
|
|
144
|
+
required.add(key);
|
|
145
|
+
for (const edge of edges(entry))
|
|
146
|
+
queue.push({ key: edge.key, optional: optional || edge.optional });
|
|
147
|
+
}
|
|
148
|
+
return { all, required };
|
|
149
|
+
}
|
|
150
|
+
function withOptional(entry, optional) {
|
|
151
|
+
const body = entry.lines.slice(1).filter(line => line !== ' optional: true');
|
|
152
|
+
const key = entry.lines[0].replace(/: \{\}$/, ':');
|
|
153
|
+
if (optional)
|
|
154
|
+
return { name: entry.name, lines: [key, ...body, ' optional: true'] };
|
|
155
|
+
return { name: entry.name, lines: body.length === 0 ? [`${key.replace(/:$/, '')}: {}`] : [key, ...body] };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The lockfile `before` plus exactly what pnpm's own resolution (`after`, from `pnpm add --lockfile-only`) adds for
|
|
159
|
+
* `dependency` in `importer`: its importer entry, the snapshots and packages only it reaches, and the optional flag
|
|
160
|
+
* removed from snapshots it now requires; an earlier version of it and what only that reached go. Everything else stays as
|
|
161
|
+
* `before` had it, so shared packages keep their resolutions (each one satisfies its own ranges already).
|
|
162
|
+
*/
|
|
163
|
+
export function mergePnpmLockfile(before, after, importer, dependency) {
|
|
164
|
+
const old = parseLock(before);
|
|
165
|
+
const resolved = parseLock(after);
|
|
166
|
+
if (formatLock(old) !== before)
|
|
167
|
+
throw new Error('init cannot rewrite this pnpm-lock.yaml exactly as pnpm wrote it');
|
|
168
|
+
for (const lock of [old, resolved]) {
|
|
169
|
+
if (lock[0]?.header !== 'lockfileVersion: \'9.0\'')
|
|
170
|
+
throw new Error(`init merges pnpm lockfile version 9.0, and this one is ${lock[0]?.header ?? 'empty'}`);
|
|
171
|
+
}
|
|
172
|
+
const header = (lock) => lock.filter(item => item.entries === null).map(item => [item.header, ...item.raw].join('\n')).join('\n');
|
|
173
|
+
if (header(old) !== header(resolved))
|
|
174
|
+
throw new Error('pnpm changed the lockfile\'s settings while adding the dependency');
|
|
175
|
+
const oldImporter = section(old, 'importers').find(entry => entry.name === importer);
|
|
176
|
+
const newImporter = section(resolved, 'importers').find(entry => entry.name === importer);
|
|
177
|
+
if (!oldImporter || !newImporter)
|
|
178
|
+
throw new Error(`pnpm-lock.yaml has no importer ${importer}`);
|
|
179
|
+
const added = importerFields(newImporter).flatMap(item => item.dependencies).find(item => item.name === dependency);
|
|
180
|
+
if (!added)
|
|
181
|
+
throw new Error(`pnpm did not add ${dependency} to ${importer}`);
|
|
182
|
+
const earlier = importerFields(oldImporter).flatMap(item => item.dependencies).find(item => item.name === dependency) ?? null;
|
|
183
|
+
// The importer: as before, with the dependency in the field pnpm put it in (fields in pnpm's order).
|
|
184
|
+
const oldFields = importerFields(oldImporter);
|
|
185
|
+
const order = importerFields(newImporter).map(item => item.field);
|
|
186
|
+
const fields = [...new Set([...order, ...oldFields.map(item => item.field)])].map(field => {
|
|
187
|
+
const kept = (oldFields.find(item => item.field === field)?.dependencies ?? []).filter(item => item.name !== dependency);
|
|
188
|
+
return { field, dependencies: field === added.field ? [...kept, added].sort(byName) : kept };
|
|
189
|
+
}).filter(item => item.dependencies.length > 0);
|
|
190
|
+
const importerEntry = { name: importer, lines: [oldImporter.lines[0].replace(/: \{\}$/, ':'), ...fields.flatMap(item => [` ${item.field}:`, ...item.dependencies.flatMap(dep => dep.lines)])] };
|
|
191
|
+
// The graph: before's snapshots, then pnpm's for keys before did not have.
|
|
192
|
+
const oldSnapshots = new Map(section(old, 'snapshots').map(entry => [entry.name, entry]));
|
|
193
|
+
const newSnapshots = new Map(section(resolved, 'snapshots').map(entry => [entry.name, entry]));
|
|
194
|
+
const merged = new Map(oldSnapshots);
|
|
195
|
+
const root = `${dependency}@${versionOf(added)}`;
|
|
196
|
+
// Walk the new dependency's tree through the merged graph, taking pnpm's entries only where before had none.
|
|
197
|
+
const queue = [root];
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
while (queue.length > 0) {
|
|
200
|
+
const key = queue.shift();
|
|
201
|
+
if (seen.has(key))
|
|
202
|
+
continue;
|
|
203
|
+
seen.add(key);
|
|
204
|
+
if (!merged.has(key)) {
|
|
205
|
+
const entry = newSnapshots.get(key);
|
|
206
|
+
if (!entry)
|
|
207
|
+
throw new Error(`pnpm's resolution references ${key}, which it does not list`);
|
|
208
|
+
merged.set(key, entry);
|
|
209
|
+
}
|
|
210
|
+
for (const edge of edges(merged.get(key)))
|
|
211
|
+
queue.push(edge.key);
|
|
212
|
+
}
|
|
213
|
+
const fieldOf = { dependencies: false, devDependencies: false, optionalDependencies: true };
|
|
214
|
+
const ours = reach(merged, [{ key: root, optional: fieldOf[added.field] ?? false }]);
|
|
215
|
+
for (const key of ours.all) {
|
|
216
|
+
const entry = merged.get(key);
|
|
217
|
+
const optional = !ours.required.has(key) && (oldSnapshots.has(key) ? entry.lines.includes(' optional: true') : true);
|
|
218
|
+
merged.set(key, withOptional(entry, optional));
|
|
219
|
+
}
|
|
220
|
+
// An earlier version of the dependency: what only it reached goes, as pnpm would prune it.
|
|
221
|
+
if (earlier && `${dependency}@${versionOf(earlier)}` !== root) {
|
|
222
|
+
const importers = section(old, 'importers').map(entry => (entry.name === importer ? importerEntry : entry));
|
|
223
|
+
const roots = importers.flatMap(entry => importerFields(entry).flatMap(item => item.dependencies
|
|
224
|
+
.map(dep => ({ dep, version: versionOf(dep) })).filter(({ version }) => !version.startsWith('link:'))
|
|
225
|
+
.map(({ dep, version }) => ({ key: /^(@[^/@]+\/)?[^@(]+@/.test(version) && !/^\d/.test(version) ? version : `${dep.name}@${version}`,
|
|
226
|
+
optional: fieldOf[dep.field] ?? false }))));
|
|
227
|
+
const live = reach(merged, roots).all;
|
|
228
|
+
for (const key of reach(oldSnapshots, [{ key: `${dependency}@${versionOf(earlier)}`, optional: false }]).all)
|
|
229
|
+
if (!live.has(key))
|
|
230
|
+
merged.delete(key);
|
|
231
|
+
}
|
|
232
|
+
// Packages: before's, less those only the earlier version's snapshots used, plus pnpm's for the new ones.
|
|
233
|
+
const used = new Set([...merged.keys()].map(packageKey));
|
|
234
|
+
const dropped = new Set([...oldSnapshots.keys()].filter(key => !merged.has(key)).map(packageKey).filter(name => !used.has(name)));
|
|
235
|
+
const oldPackages = section(old, 'packages');
|
|
236
|
+
const listed = new Set(oldPackages.map(entry => entry.name));
|
|
237
|
+
const newPackages = new Map(section(resolved, 'packages').map(entry => [entry.name, entry]));
|
|
238
|
+
const packages = oldPackages.filter(entry => !dropped.has(entry.name));
|
|
239
|
+
for (const name of used) {
|
|
240
|
+
if (listed.has(name))
|
|
241
|
+
continue;
|
|
242
|
+
const entry = newPackages.get(name);
|
|
243
|
+
if (!entry)
|
|
244
|
+
throw new Error(`pnpm's resolution lists no package ${name}`);
|
|
245
|
+
packages.push(entry);
|
|
246
|
+
}
|
|
247
|
+
return formatLock(old.map(item => {
|
|
248
|
+
if (item.header === 'importers:')
|
|
249
|
+
return { ...item, entries: section(old, 'importers').map(entry => (entry.name === importer ? importerEntry : entry)) };
|
|
250
|
+
if (item.header === 'packages:')
|
|
251
|
+
return { ...item, entries: packages.sort(byName) };
|
|
252
|
+
if (item.header === 'snapshots:')
|
|
253
|
+
return { ...item, entries: [...merged.values()].sort(byName) };
|
|
254
|
+
return item;
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
/** Entries as keys and their content, per format; the importer's own entry under IMPORTER. */
|
|
258
|
+
const IMPORTER = '\u0000importer';
|
|
259
|
+
function entriesOf(format, text, importer) {
|
|
260
|
+
const out = new Map();
|
|
261
|
+
if (format === 'pnpm') {
|
|
262
|
+
for (const item of parseLock(text)) {
|
|
263
|
+
if (item.entries === null) {
|
|
264
|
+
out.set(`header ${item.header}`, item.raw.join('\n'));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
for (const entry of item.entries) {
|
|
268
|
+
const name = item.header === 'importers:' && entry.name === importer ? IMPORTER : `${item.header} ${entry.name}`;
|
|
269
|
+
// The optional flag is the dependency's to clear (it now requires the package): compared without it.
|
|
270
|
+
out.set(name, entry.lines.filter(line => line !== ' optional: true').join('\n').replace(/: \{\}$/, ':'));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (format === 'npm') {
|
|
275
|
+
const packages = JSON.parse(text).packages ?? {};
|
|
276
|
+
for (const [key, value] of Object.entries(packages))
|
|
277
|
+
out.set(key === (importer === '.' ? '' : importer) ? IMPORTER : key, JSON.stringify(value));
|
|
278
|
+
}
|
|
279
|
+
else if (format === 'yarn') {
|
|
280
|
+
for (const block of text.split(/\n\s*\n/)) {
|
|
281
|
+
const lines = block.split('\n').filter(line => line.trim() !== '' && !line.startsWith('#'));
|
|
282
|
+
if (lines.length === 0 || /^\s/.test(lines[0]))
|
|
283
|
+
continue;
|
|
284
|
+
for (const descriptor of lines[0].replace(/:$/, '').split(/,\s*/)) {
|
|
285
|
+
const name = unquote(descriptor);
|
|
286
|
+
out.set(/@workspace:/.test(name) && name.endsWith(`@workspace:${importer}`) ? IMPORTER : name, lines.slice(1).join('\n'));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const parsed = JSON.parse(text.replace(/,(\s*[}\]])/g, '$1'));
|
|
292
|
+
for (const [key, value] of Object.entries(parsed.workspaces ?? {}))
|
|
293
|
+
out.set(key === (importer === '.' ? '' : importer) ? IMPORTER : `workspace ${key}`, JSON.stringify(value));
|
|
294
|
+
for (const [key, value] of Object.entries(parsed.packages ?? {}))
|
|
295
|
+
out.set(`package ${key}`, JSON.stringify(value));
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
/** The entries both lockfiles list whose content adding `dependency` to `importer` changed, other than the importer's own
|
|
300
|
+
* and the dependency's own: what that must never touch. (An entry only an earlier version of the dependency used may go;
|
|
301
|
+
* a moved resolution always shows in the entries that reference it.) */
|
|
302
|
+
export function lockfileDrift(format, before, after, importer, dependency) {
|
|
303
|
+
const a = entriesOf(format, before, importer);
|
|
304
|
+
const b = entriesOf(format, after, importer);
|
|
305
|
+
return [...a.entries()].filter(([key, value]) => key !== IMPORTER && !key.includes(dependency) && b.has(key) && b.get(key) !== value)
|
|
306
|
+
.map(([key]) => key);
|
|
307
|
+
}
|
|
@@ -103,6 +103,12 @@ export function parseOnboardingStatus(value, expected) {
|
|
|
103
103
|
if (!Array.isArray(value.services) || !value.services.every(service => isRecord(service) && typeof service.id === 'string'
|
|
104
104
|
&& SERVICE_ENGINES.includes(service.engine)))
|
|
105
105
|
invalid('its services');
|
|
106
|
+
const review = value.review;
|
|
107
|
+
if (!(review === null || (isRecord(review) && (review.verdict === 'accept'
|
|
108
|
+
|| (review.verdict === 'skipped' && typeof review.reason === 'string' && review.reason.length > 0)))))
|
|
109
|
+
invalid('its review');
|
|
110
|
+
if (value.state !== 'ready' && review !== null)
|
|
111
|
+
invalid('its review');
|
|
106
112
|
if (value.block !== null)
|
|
107
113
|
parseOnboardingBlock(value.block);
|
|
108
114
|
const checkpoint = value.checkpoint;
|
|
@@ -311,8 +317,18 @@ export function formatOnboarding(status) {
|
|
|
311
317
|
const outside = [...status.standIns, ...status.unsetProviders, ...standInGapNotes(status.standInGaps)];
|
|
312
318
|
if (outside.length)
|
|
313
319
|
lines.push('', ...outside.map(note => chalk.yellow(`Note: ${safe(note)}`)));
|
|
320
|
+
const unreviewed = reviewNotes(status.review);
|
|
321
|
+
if (unreviewed.length)
|
|
322
|
+
lines.push('', ...unreviewed.map(note => chalk.yellow(`Note: ${safe(note)}`)));
|
|
314
323
|
return lines.join('\n');
|
|
315
324
|
}
|
|
325
|
+
/** Rule 17 (amendment 32): an onboarding the independent review skipped, as the sentence every result shows; none otherwise. */
|
|
326
|
+
export function reviewNotes(review) {
|
|
327
|
+
if (review?.verdict !== 'skipped')
|
|
328
|
+
return [];
|
|
329
|
+
return [`This onboarding was NOT reviewed: the independent review that checks the setup did not cheat is turned off for this `
|
|
330
|
+
+ `team (${review.reason}).`];
|
|
331
|
+
}
|
|
316
332
|
/** Rule 15(g), amendment 3: a stand-in gap as the sentence every result shows. */
|
|
317
333
|
export function standInGapNotes(gaps) {
|
|
318
334
|
return gaps.map(gap => `A stand-in could not serve ${gap} while onboarding signed in and did the workflow; the crawl may meet it too.`);
|
package/dist/commands/verify.js
CHANGED
|
@@ -31,7 +31,7 @@ import { classifyHttpError, readWithRetries, SERVICE_SILENCE_LIMIT_MS, ServiceSi
|
|
|
31
31
|
import { GATEWAY_TIMEOUT_MS, gatewayFetch } from './case-batch.js';
|
|
32
32
|
import { CRAWL_BRIEF_MAX_FUNCTIONS, CRAWL_BRIEF_MAX_GROUPS, CRAWL_BRIEF_MAX_PRODUCTION_ROWS, CRAWL_BUDGET_MAX_MS, CRAWL_BUDGET_MIN_MS, CRAWL_MAX_FINDINGS, CRAWL_MAX_FINDING_STEPS, CRAWL_MAX_IDEAS, CRAWL_MAX_STAND_IN_LINES, CRAWL_MAX_STAND_IN_ROWS, CRAWL_MAX_TEXT_CHARS, CRAWL_WAIT_MAX_MS, CRAWL_POOLS, } from './crawl-contract.js';
|
|
33
33
|
import { captureCheckout, crawlUnavailableText, EXPLICIT_WALL_MS, normalModeFailure, postPrecomputeCapture, } from './verify-precompute.js';
|
|
34
|
-
import { formatOnboarding, onboardingExitCode, readOnboardingStatus, reportOnboardingState, standInGapNotes, waitForOnboarding } from './verify-onboarding.js';
|
|
34
|
+
import { formatOnboarding, onboardingExitCode, readOnboardingStatus, reportOnboardingState, reviewNotes, standInGapNotes, waitForOnboarding, } from './verify-onboarding.js';
|
|
35
35
|
import { formatCapture, readPreVerifyCapture } from './capture-brief.js';
|
|
36
36
|
const CRAWLS_PATH = '/api/agent/cloud-verifier/crawls';
|
|
37
37
|
/** A held read (waitAfter): the service's hold, then the time any read gets. */
|
|
@@ -555,6 +555,52 @@ export function formatCrawl(view) {
|
|
|
555
555
|
}
|
|
556
556
|
return lines.join('\n');
|
|
557
557
|
}
|
|
558
|
+
export function verifyReport(view) {
|
|
559
|
+
const status = reportedStatus(view);
|
|
560
|
+
const published = results(view);
|
|
561
|
+
const error = published?.error ?? view.error;
|
|
562
|
+
const explored = published !== null && view.clocks.crawlStartedAt !== null;
|
|
563
|
+
const findings = explored ? VERDICTS.flatMap(verdict => published.findings.filter(finding => finding.verdict === verdict)) : [];
|
|
564
|
+
const ideaWords = { covered: 'tried', blocked: 'could not try', unfinished: 'not finished' };
|
|
565
|
+
return {
|
|
566
|
+
runId: view.runId,
|
|
567
|
+
repository: view.repository,
|
|
568
|
+
title: view.changeTitle,
|
|
569
|
+
baseCommit: view.baseCommit,
|
|
570
|
+
workCommit: view.workCommit,
|
|
571
|
+
status,
|
|
572
|
+
headline: headline(view),
|
|
573
|
+
bugs: published?.bugs ?? view.totals.bugs,
|
|
574
|
+
findings: findings.map(finding => ({
|
|
575
|
+
id: finding.id,
|
|
576
|
+
verdict: finding.verdict,
|
|
577
|
+
why: finding.why,
|
|
578
|
+
start: finding.start,
|
|
579
|
+
steps: finding.steps,
|
|
580
|
+
outsideServices: finding.standIns && (finding.standIns.old.length > 0 || finding.standIns.new.length > 0)
|
|
581
|
+
? { withoutChange: finding.standIns.old, withChange: finding.standIns.new }
|
|
582
|
+
: null,
|
|
583
|
+
})),
|
|
584
|
+
spots: explored ? published.reach.spots.map(spot => ({
|
|
585
|
+
file: spot.file,
|
|
586
|
+
line: spot.line,
|
|
587
|
+
reached: spot.furthest === null ? 'never reached' : SPOT_WORDS[spot.furthest],
|
|
588
|
+
reachedBy: spot.reachedBy && { start: spot.reachedBy.start, steps: spot.reachedBy.steps, page: spot.reachedBy.page },
|
|
589
|
+
findingIds: spot.findingIds,
|
|
590
|
+
})) : [],
|
|
591
|
+
ideas: explored && published.ideas !== undefined
|
|
592
|
+
? published.ideas.map(idea => ({ text: idea.text, result: ideaWords[idea.state], reason: idea.reason, findingIds: idea.findingIds }))
|
|
593
|
+
: null,
|
|
594
|
+
neverRan: explored ? {
|
|
595
|
+
files: published.reach.unreachedChangedFiles,
|
|
596
|
+
functions: published.reach.blast === null ? null : { ran: published.reach.blast.ran, total: published.reach.blast.functions },
|
|
597
|
+
} : null,
|
|
598
|
+
notFinishedInTime: explored ? published.notFinishedInTime : 0,
|
|
599
|
+
outsideServiceGaps: explored ? published.standIns?.gaps ?? [] : [],
|
|
600
|
+
error: error ? { code: error.code, message: error.message } : null,
|
|
601
|
+
machinesNotProvenShutDown: TERMINAL.has(view.status) ? view.totals.cleanupUnproven : 0,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
558
604
|
/** Case-batch status conventions: 0 finished (or answered: amendment 9, its work
|
|
559
605
|
* is done and only its shutdown remains), or still running with --no-wait;
|
|
560
606
|
* 2 ended without finishing (incomplete, cancelled) or finished with cleanup
|
|
@@ -791,9 +837,9 @@ function checkedLine(change) {
|
|
|
791
837
|
}
|
|
792
838
|
/** Prints the onboarding (and nothing else) as the command's answer: `--json`
|
|
793
839
|
* carries OnboardingStatusV1. A block exits 3: its ask says what to do. */
|
|
794
|
-
function finishWithOnboarding(status,
|
|
795
|
-
if (json)
|
|
796
|
-
|
|
840
|
+
function finishWithOnboarding(status, options, change) {
|
|
841
|
+
if (options.json || options.raw)
|
|
842
|
+
printVerifyJson(options, change, status, null);
|
|
797
843
|
else
|
|
798
844
|
console.log(formatOnboarding(status));
|
|
799
845
|
process.exitCode = onboardingExitCode(status);
|
|
@@ -997,7 +1043,7 @@ export async function preVerifyCommand(options) {
|
|
|
997
1043
|
if (options.wait !== false && briefOf(view) === null && !TERMINAL.has(reportedStatus(view)))
|
|
998
1044
|
view = await waitForBrief(view, identity, auth.token);
|
|
999
1045
|
const brief = briefOf(view);
|
|
1000
|
-
// CAPTURE-V1 rule 7b: the routes the change touches, with their share of captured sessions; null without capture set up.
|
|
1046
|
+
// CAPTURE-V1 rule 7b: the routes the change touches, with their share of captured sessions, per app; null without capture set up.
|
|
1001
1047
|
const captured = await readPreVerifyCapture(findGitRoot(), identity.repository, auth.token, [...new Set([...(brief?.functions.map(fn => fn.file) ?? []), ...change.files])], new Date());
|
|
1002
1048
|
if (options.json)
|
|
1003
1049
|
finish(onboarding, view, captured);
|
|
@@ -1014,13 +1060,22 @@ export async function preVerifyCommand(options) {
|
|
|
1014
1060
|
: view.status === 'completed' ? 'It was checked before briefs existed and is not run again: change anything in the checkout and run `haystack pre-verify` again.'
|
|
1015
1061
|
: 'Run `haystack pre-verify` again to prepare it again.'));
|
|
1016
1062
|
if (!options.json && captured !== null)
|
|
1017
|
-
|
|
1063
|
+
for (const section of captured)
|
|
1064
|
+
console.log(`\n${formatCapture(section)}`);
|
|
1018
1065
|
if (brief === null && TERMINAL.has(reportedStatus(view)))
|
|
1019
1066
|
process.exitCode = 2;
|
|
1020
1067
|
}
|
|
1068
|
+
/** verify's one JSON document: the report (`--json`) or the service's whole record (`--raw`). */
|
|
1069
|
+
function printVerifyJson(options, change, onboarding, view) {
|
|
1070
|
+
const document = options.raw
|
|
1071
|
+
? withSchema('verify-raw', { change, onboarding, crawl: view })
|
|
1072
|
+
: withSchema('verify', { change, onboarding, crawl: view === null ? null : verifyReport(view) });
|
|
1073
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}\n`);
|
|
1074
|
+
}
|
|
1021
1075
|
export async function verifyCommand(options) {
|
|
1076
|
+
const json = options.json || options.raw;
|
|
1022
1077
|
// In --json mode stdout carries the one document; every other line goes to stderr.
|
|
1023
|
-
const note = (line) => { if (
|
|
1078
|
+
const note = (line) => { if (json)
|
|
1024
1079
|
console.error(line);
|
|
1025
1080
|
else
|
|
1026
1081
|
console.log(line); };
|
|
@@ -1030,8 +1085,8 @@ export async function verifyCommand(options) {
|
|
|
1030
1085
|
const change = checkedChange(request.baseCommit, request.workCommit);
|
|
1031
1086
|
if (!request.crawl) {
|
|
1032
1087
|
note(`Nothing to verify: this checkout has no changes against ${request.baseCommit.slice(0, 12)}.`);
|
|
1033
|
-
if (
|
|
1034
|
-
|
|
1088
|
+
if (json)
|
|
1089
|
+
printVerifyJson(options, change, null, null);
|
|
1035
1090
|
return;
|
|
1036
1091
|
}
|
|
1037
1092
|
note(checkedLine(change));
|
|
@@ -1065,7 +1120,7 @@ export async function verifyCommand(options) {
|
|
|
1065
1120
|
reportOnboardingState(onboarding);
|
|
1066
1121
|
}
|
|
1067
1122
|
if (onboarding.state !== 'ready') {
|
|
1068
|
-
finishWithOnboarding(onboarding, options
|
|
1123
|
+
finishWithOnboarding(onboarding, options, change);
|
|
1069
1124
|
return;
|
|
1070
1125
|
}
|
|
1071
1126
|
// Ready: the service admitted the waiting crawl when the record published; this
|
|
@@ -1075,8 +1130,8 @@ export async function verifyCommand(options) {
|
|
|
1075
1130
|
note(submitted.line);
|
|
1076
1131
|
}
|
|
1077
1132
|
if (submitted.runId === null) {
|
|
1078
|
-
if (
|
|
1079
|
-
|
|
1133
|
+
if (json)
|
|
1134
|
+
printVerifyJson(options, change, onboarding, null);
|
|
1080
1135
|
process.exitCode = 1;
|
|
1081
1136
|
return;
|
|
1082
1137
|
}
|
|
@@ -1089,12 +1144,12 @@ export async function verifyCommand(options) {
|
|
|
1089
1144
|
if (options.wait !== false && !TERMINAL.has(reportedStatus(view))) {
|
|
1090
1145
|
view = await waitForCrawl(view, identity, auth.token);
|
|
1091
1146
|
}
|
|
1092
|
-
if (
|
|
1093
|
-
|
|
1147
|
+
if (json)
|
|
1148
|
+
printVerifyJson(options, change, onboarding, view);
|
|
1094
1149
|
else {
|
|
1095
1150
|
console.log(formatCrawl(view));
|
|
1096
1151
|
const notes = [...(onboarding?.dataAbsences ?? []), ...(onboarding?.versionChoices ?? []), ...(onboarding?.standIns ?? []),
|
|
1097
|
-
...(onboarding?.unsetProviders ?? []), ...standInGapNotes(onboarding?.standInGaps ?? [])];
|
|
1152
|
+
...(onboarding?.unsetProviders ?? []), ...standInGapNotes(onboarding?.standInGaps ?? []), ...reviewNotes(onboarding?.review ?? null)];
|
|
1098
1153
|
if (notes.length)
|
|
1099
1154
|
console.log(['', ...notes.map(note => `Note: ${safe(note)}`)].join('\n'));
|
|
1100
1155
|
}
|