@haystackeditor/cli 0.23.3 → 0.24.1

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.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Next.js production telemetry without an application SDK.
3
+ *
4
+ * // next.config.ts
5
+ * import { withHaystackTelemetry } from '@haystackeditor/cli/next';
6
+ * export default withHaystackTelemetry(nextConfig);
7
+ *
8
+ * `next build` (Turbopack or --webpack) then instruments every git-tracked
9
+ * server source module as the bundler compiles it, and writes the runtime
10
+ * beside the build in `<distDir>/.haystack-telemetry/`. Start the server with
11
+ *
12
+ * NODE_OPTIONS=--require=<abs distDir>/.haystack-telemetry/register.cjs
13
+ *
14
+ * plus the usual HAYSTACK_TELEMETRY / _ENDPOINT / _TOKEN variables; without
15
+ * them every probe is a pass-through.
16
+ *
17
+ * Why this shape:
18
+ * - Source, not output: Next packs server code into chunks that mirror no
19
+ * source file, so `haystack telemetry instrument` cannot map them. A loader
20
+ * sees each module's real path and text, so every site records its
21
+ * repository path and line, which is what joins it to a change's functions.
22
+ * - Loaders run in bundler worker processes (Turbopack always), so they hand
23
+ * each module's sites to the post-compile step through a per-build session
24
+ * directory, not memory. The session id travels in the environment, which
25
+ * every process `next build` spawns after loading this config inherits.
26
+ * - The runtime stays outside the bundle and is installed by a preload, which
27
+ * runs before the server's first module under `next start` and standalone
28
+ * `server.js` alike, so no customer source file (instrumentation.ts) changes.
29
+ * - Only `next build` is instrumented; dev and `next start` get the config
30
+ * untouched.
31
+ */
32
+ import { execFileSync } from 'node:child_process';
33
+ import { createHash, randomBytes } from 'node:crypto';
34
+ import { existsSync, readdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
35
+ import { tmpdir } from 'node:os';
36
+ import { join, resolve } from 'node:path';
37
+ import { fileURLToPath } from 'node:url';
38
+ import { authenticateTelemetryRuntime, BUNDLED_SOURCE_EXTENSIONS, writeBundledTelemetryRuntime, } from '../commands/telemetry.js';
39
+ const PRODUCTION_BUILD_PHASE = 'phase-production-build';
40
+ const SESSION_ENVIRONMENT = '__HAYSTACK_TELEMETRY_NEXT_SESSION';
41
+ // Must equal the directory names in ./next-loader.cts.
42
+ const MODULES_DIRECTORY = 'modules';
43
+ const UNPARSED_DIRECTORY = 'unparsed';
44
+ function readRecords(directory) {
45
+ if (!existsSync(directory))
46
+ return [];
47
+ return readdirSync(directory)
48
+ .filter(name => name.endsWith('.json'))
49
+ .map(name => JSON.parse(readFileSync(join(directory, name), 'utf8')));
50
+ }
51
+ const LOADER_PATH = fileURLToPath(new URL('./next-loader.cjs', import.meta.url));
52
+ function plainObject(value) {
53
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
54
+ }
55
+ function gitOutput(cwd, args) {
56
+ try {
57
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 * 1024 });
58
+ }
59
+ catch (error) {
60
+ throw new Error(`Haystack telemetry needs the app's git checkout to name source files (git ${args.join(' ')} failed in ${cwd}): ` +
61
+ `${error instanceof Error ? error.message : String(error)}`);
62
+ }
63
+ }
64
+ export function withHaystackTelemetry(nextConfig) {
65
+ return async (phase, context) => {
66
+ const resolved = typeof nextConfig === 'function' ? await nextConfig(phase, context) : nextConfig;
67
+ return phase === PRODUCTION_BUILD_PHASE ? instrumentProductionBuild(resolved) : resolved;
68
+ };
69
+ }
70
+ function instrumentProductionBuild(config) {
71
+ if (plainObject(config.experimental).turbopackFileSystemCacheForBuild) {
72
+ // A persisted Turbopack result skips the loader, so that module's sites
73
+ // would never reach the manifest and its probes would count nothing.
74
+ throw new Error('Haystack telemetry cannot instrument a build that reuses cached Turbopack modules; turn off experimental.turbopackFileSystemCacheForBuild');
75
+ }
76
+ // Site paths are repository-relative so they match the crawl's blast radius
77
+ // (`apps/web/src/...` in a monorepo), not relative to the Next project.
78
+ const sourceRoot = realpathSync(gitOutput(process.cwd(), ['rev-parse', '--show-toplevel']).trim());
79
+ // Evaluating this config has no side effects beyond naming the session:
80
+ // `next build` re-evaluates it in every worker it spawns (page-data and
81
+ // static-generation workers too), and loaders create what they write.
82
+ const session = process.env[SESSION_ENVIRONMENT] ||= randomBytes(12).toString('hex');
83
+ const sessionDirectory = join(tmpdir(), `haystack-telemetry-next-${session}`);
84
+ // The channel only has to be unique per instrumented build sharing a
85
+ // process; deriving it from the checkout keeps identical builds identical.
86
+ const controlChannel = `__haystackTelemetryControl_${createHash('sha256')
87
+ .update(`next\0${sourceRoot}`).digest('hex').slice(0, 24)}`;
88
+ const loaderOptions = {
89
+ sourceRoot,
90
+ sessionDirectory,
91
+ controlChannel,
92
+ runtimeIntegrity: authenticateTelemetryRuntime(controlChannel).integrity,
93
+ };
94
+ const loader = { loader: LOADER_PATH, options: loaderOptions };
95
+ // Turbopack (Next 16's default bundler): server-side ("node") modules outside
96
+ // node_modules ("foreign"). Edge and browser contexts are left alone: the
97
+ // runtime is Node-only.
98
+ const turbopack = plainObject(config.turbopack);
99
+ const rules = { ...plainObject(turbopack.rules) };
100
+ for (const extension of BUNDLED_SOURCE_EXTENSIONS) {
101
+ const pattern = `*${extension}`;
102
+ const rule = { condition: { all: ['node', { not: 'foreign' }] }, loaders: [loader] };
103
+ const existing = rules[pattern];
104
+ rules[pattern] = existing === undefined ? rule : [rule, ...(Array.isArray(existing) ? existing : [existing])];
105
+ }
106
+ const originalWebpack = typeof config.webpack === 'function' ? config.webpack : null;
107
+ const compiler = plainObject(config.compiler);
108
+ const originalAfterCompile = typeof compiler.runAfterProductionCompile === 'function'
109
+ ? compiler.runAfterProductionCompile
110
+ : null;
111
+ return {
112
+ ...config,
113
+ turbopack: { ...turbopack, rules },
114
+ // webpack (`next build --webpack`): the Node server compilation only, as
115
+ // a pre-loader so it sees the source before SWC strips types and lines.
116
+ webpack(webpackConfig, context) {
117
+ const applied = originalWebpack ? originalWebpack(webpackConfig, context) : webpackConfig;
118
+ const addRule = (resolved) => {
119
+ if (context?.isServer !== true || context?.nextRuntime !== 'nodejs')
120
+ return resolved;
121
+ const moduleConfig = plainObject(resolved.module);
122
+ moduleConfig.rules = [{
123
+ test: /\.(?:[cm]?[jt]sx?)$/,
124
+ exclude: /[\\/]node_modules[\\/]/,
125
+ enforce: 'pre',
126
+ use: [loader],
127
+ }, ...(Array.isArray(moduleConfig.rules) ? moduleConfig.rules : [])];
128
+ resolved.module = moduleConfig;
129
+ return resolved;
130
+ };
131
+ return applied instanceof Promise ? applied.then(addRule) : addRule(applied);
132
+ },
133
+ compiler: {
134
+ ...compiler,
135
+ async runAfterProductionCompile(context) {
136
+ if (originalAfterCompile)
137
+ await originalAfterCompile(context);
138
+ writeNextTelemetryRuntime(loaderOptions, resolve(context.projectDir, context.distDir));
139
+ },
140
+ },
141
+ };
142
+ }
143
+ function writeNextTelemetryRuntime(options, distDirectory) {
144
+ const modules = readRecords(join(options.sessionDirectory, MODULES_DIRECTORY));
145
+ const unparsed = readRecords(join(options.sessionDirectory, UNPARSED_DIRECTORY));
146
+ if (modules.length === 0) {
147
+ throw new Error(`Haystack telemetry: the bundler compiled no instrumentable tracked server module through its loader${unparsed.length > 0 ? ` (${unparsed.length} could not be parsed)` : ''}; nothing was instrumented`);
148
+ }
149
+ const { registerPath, manifest } = writeBundledTelemetryRuntime({
150
+ outputDirectory: distDirectory,
151
+ controlChannel: options.controlChannel,
152
+ bundler: 'next',
153
+ sourceRoot: options.sourceRoot,
154
+ modules,
155
+ unparsed,
156
+ });
157
+ // The session is spent. Its id stays in the environment so the workers
158
+ // `next build` spawns next resolve the same (now empty) session.
159
+ rmSync(options.sessionDirectory, { recursive: true, force: true });
160
+ console.log(`Haystack telemetry: instrumented ${modules.length} server module(s), ${manifest.sites.length} site(s); build ${manifest.build_id}.`);
161
+ if (manifest.skipped_unparsed_files.length > 0) {
162
+ // Named, never silent: these modules run but count nothing.
163
+ console.log(`Haystack telemetry: skipped ${manifest.skipped_unparsed_files.length} server module(s) Babel could not parse (shipped uninstrumented; listed in instrumentation.json skipped_unparsed_files):`);
164
+ for (const file of manifest.skipped_unparsed_files)
165
+ console.log(` ${file.source_path}: ${file.reason}`);
166
+ }
167
+ console.log(`Haystack telemetry: start the server with NODE_OPTIONS=--require=${registerPath}`);
168
+ }
@@ -41,8 +41,15 @@ export function transientReadFailure(error) {
41
41
  return [429, 502, 503, 504].includes(error.status) ? `HTTP ${error.status}` : null;
42
42
  if (error instanceof Error && error.name === 'TimeoutError')
43
43
  return 'no answer in time';
44
- return error instanceof TypeError ? transportCause(error.cause, 0) : null;
45
- }
44
+ return error instanceof TypeError ? transportCause(causeOf(error), 0) : null;
45
+ }
46
+ /** An error's cause and, for an AggregateError, its inner errors, read structurally rather than through ES2022's Error.cause
47
+ * and AggregateError typings: the frontend's build type-checks this file against ES2020 (#3854 broke deploy-frontend). */
48
+ const causeOf = (error) => error.cause;
49
+ const innerErrors = (error) => {
50
+ const errors = error.errors;
51
+ return Array.isArray(errors) ? errors : [];
52
+ };
46
53
  /** The transport failure somewhere in a fetch TypeError's cause chain. A connection dropped while the body is read fails as
47
54
  * TypeError "terminated", whose cause may be the socket error or the request's TimeoutError (Bugbot, #3760); several
48
55
  * addresses tried at once (IPv4 and IPv6) fail as one AggregateError. */
@@ -54,14 +61,12 @@ function transportCause(cause, depth) {
54
61
  const code = cause.code;
55
62
  if (typeof code === 'string' && TRANSPORT_FAILURE_CODES.has(code))
56
63
  return `no answer: ${code}`;
57
- if (cause instanceof AggregateError) {
58
- for (const inner of cause.errors) {
59
- const found = transportCause(inner, depth + 1);
60
- if (found !== null)
61
- return found;
62
- }
64
+ for (const inner of innerErrors(cause)) {
65
+ const found = transportCause(inner, depth + 1);
66
+ if (found !== null)
67
+ return found;
63
68
  }
64
- return transportCause(cause.cause, depth + 1);
69
+ return transportCause(causeOf(cause), depth + 1);
65
70
  }
66
71
  /** How long a wait keeps asking while the service gives no answer, counted from its last answer. Stall detection, not a
67
72
  * cap on the work: a service that answers is followed for as long as the work runs. */
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.23.3",
3
+ "version": "0.24.1",
4
4
  "description": "haystack verify: run your app with and without a change, and see what the change broke",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "haystack": "dist/index.js"
8
8
  },
9
+ "exports": {
10
+ "./next": {
11
+ "types": "./dist/telemetry/next.d.ts",
12
+ "default": "./dist/telemetry/next.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
9
16
  "scripts": {
10
- "build": "rm -rf dist && tsc && cp -r src/assets dist/assets",
17
+ "build": "rm -rf dist && tsc && tsc -p tsconfig.next-types.json && cp -r src/assets dist/assets",
11
18
  "dev": "tsc --watch",
12
19
  "lint": "eslint src",
13
20
  "start": "node dist/index.js",
@@ -2,12 +2,12 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://docs.haystackeditor.com/cli/schemas/init.v1.json",
4
4
  "title": "Haystack init: this repository's setup for `haystack verify`",
5
- "description": "Machine-readable output of `haystack init --json`. `changes` are the project-local changes init planned (made when status is done); `onboarding` is the base's OnboardingStatusV1 (onboarding_contracts.ts, APP-ONBOARDING-V1) as POST /api/agent/cloud-verifier/onboarding returns it, null unless status is done. Exit codes: 0 done (onboarding running or ready), 1 failed (the error envelope), 2 planned (nothing written; run with --yes), 3 done but onboarding blocked, 4 login-required, 5 app-required (install the Haystack GitHub App), 6 done but onboarding stopped before finishing (run init again).",
5
+ "description": "Machine-readable output of `haystack init --json`. `changes` are the project-local changes init planned (made when status is done); `onboarding` is the base's OnboardingStatusV1 (onboarding_contracts.ts, APP-ONBOARDING-V1) as POST /api/agent/cloud-verifier/onboarding returns it, null unless status is done. `notes` says what became of `--notes`: \"written\" (the repository's onboarding notes are now these), \"unchanged\" (they already were), null without `--notes` or when init stopped before writing them. Exit codes: 0 done (onboarding running or ready), 1 failed (the error envelope), 2 planned (nothing written; run with --yes), 3 done but onboarding blocked, 4 login-required, 5 app-required (install the Haystack GitHub App), 6 done but onboarding stopped before finishing (run init again).",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["schema_version", "status", "repository", "changes", "notices", "onboarding"],
8
+ "required": ["schema_version", "status", "repository", "changes", "notices", "notes", "onboarding"],
9
9
  "properties": {
10
- "schema_version": { "const": "1.0.0" },
10
+ "schema_version": { "const": "1.0.1" },
11
11
  "status": { "enum": ["done", "planned", "login-required", "app-required"] },
12
12
  "repository": { "type": "string" },
13
13
  "changes": {
@@ -25,6 +25,7 @@
25
25
  }
26
26
  },
27
27
  "notices": { "type": "array", "items": { "type": "string" } },
28
+ "notes": { "oneOf": [{ "type": "null" }, { "enum": ["written", "unchanged"] }] },
28
29
  "onboarding": {
29
30
  "oneOf": [
30
31
  { "type": "null" },
@@ -0,0 +1,178 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://docs.haystackeditor.com/cli/schemas/pre-verify.v1.json",
4
+ "title": "haystack pre-verify --json",
5
+ "description": "CRAWL-V1 amendment 18: the brief of the current change before any crawl: the functions it touches (the front half's blast radius, nearest first) and, when incident planning ran, Haystack's own ideas of what could break. `run` is the run of this capture that holds or will hold the brief; `brief` is null until it does (or when the run ended without one).",
6
+ "type": "object",
7
+ "required": ["schema_version", "change", "onboarding", "run", "brief"],
8
+ "properties": {
9
+ "schema_version": { "const": "1.0.1" },
10
+ "change": {
11
+ "type": "object",
12
+ "required": ["baseCommit", "workCommit", "files"],
13
+ "properties": {
14
+ "baseCommit": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
15
+ "workCommit": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
16
+ "files": { "type": "array", "items": { "type": "string" } }
17
+ }
18
+ },
19
+ "onboarding": { "description": "The app's onboarding for this base when this command met it (see `haystack schema verify-onboarding`), else null.", "oneOf": [{ "type": "null" }, { "type": "object" }] },
20
+ "run": {
21
+ "oneOf": [
22
+ { "type": "null" },
23
+ {
24
+ "type": "object",
25
+ "required": ["runId", "status"],
26
+ "properties": {
27
+ "runId": { "type": "string", "pattern": "^cv_[0-9a-f]{48}$" },
28
+ "status": { "enum": ["queued", "preparing", "crawling", "draining", "completed", "prepared", "incomplete", "cancelled"] }
29
+ }
30
+ }
31
+ ]
32
+ },
33
+ "brief": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/brief" }] }
34
+ },
35
+ "$defs": {
36
+ "brief": {
37
+ "type": "object",
38
+ "required": ["functions", "leftOut", "planning"],
39
+ "properties": {
40
+ "functions": {
41
+ "description": "Every affected function the front half listed (at most 2000), nearest first. distance 0 is edited by the change; 1 is one engine edge away.",
42
+ "type": "array",
43
+ "maxItems": 2000,
44
+ "items": {
45
+ "type": "object",
46
+ "required": ["file", "name", "lines", "distance", "direction"],
47
+ "properties": {
48
+ "file": { "type": "string" },
49
+ "name": { "type": "string" },
50
+ "lines": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "type": "integer", "minimum": 0 } },
51
+ "distance": { "type": "integer", "minimum": 0 },
52
+ "direction": { "enum": ["changed", "upstream", "downstream"] }
53
+ }
54
+ }
55
+ },
56
+ "leftOut": {
57
+ "description": "Functions the blast radius found but did not list, counted with why.",
58
+ "type": "array",
59
+ "items": { "type": "object", "required": ["reason", "count"], "properties": { "reason": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } }
60
+ },
61
+ "production": {
62
+ "description": "CRAWL-V1 amendment 19, only when the repository's own Haystack telemetry holds data in the last 7 days: how the brief's functions ran in production, by their index in `functions` (production runs the code before the change; lines are moved through the change's diff). A function with no row is unknown, not unused.",
63
+ "type": "object",
64
+ "required": ["windowDays", "payloads", "newestAt", "partial", "functions"],
65
+ "properties": {
66
+ "windowDays": { "type": "integer", "minimum": 1 },
67
+ "payloads": { "type": "integer", "minimum": 1 },
68
+ "newestAt": { "type": "string" },
69
+ "partial": { "type": "boolean", "description": "The read stopped at its byte budget: every count is a lower bound." },
70
+ "functions": {
71
+ "type": "array",
72
+ "items": {
73
+ "type": "object",
74
+ "required": ["index", "branches", "branchesOmitted", "values", "valuesOmitted"],
75
+ "properties": {
76
+ "index": { "type": "integer", "minimum": 0 },
77
+ "branches": {
78
+ "type": "array",
79
+ "maxItems": 100,
80
+ "items": {
81
+ "type": "object",
82
+ "required": ["line", "whenTrue", "whenFalse", "capped"],
83
+ "properties": {
84
+ "line": { "type": "integer", "minimum": 1 },
85
+ "whenTrue": { "type": "integer", "minimum": 0 },
86
+ "whenFalse": { "type": "integer", "minimum": 0 },
87
+ "capped": { "type": "boolean", "description": "A counter hit its cap: the counts are lower bounds." }
88
+ }
89
+ }
90
+ },
91
+ "branchesOmitted": { "type": "integer", "minimum": 0 },
92
+ "values": {
93
+ "type": "array",
94
+ "maxItems": 100,
95
+ "items": {
96
+ "type": "object",
97
+ "required": ["line", "what", "shapes"],
98
+ "properties": {
99
+ "line": { "type": "integer", "minimum": 1 },
100
+ "what": { "type": "string", "description": "The value observed, e.g. parameter:email or binding:options." },
101
+ "shapes": {
102
+ "type": "array",
103
+ "items": {
104
+ "type": "object",
105
+ "required": ["shape", "count"],
106
+ "properties": {
107
+ "shape": { "type": "string", "description": "A value-free category, e.g. undefined, string:empty:empty, array:few." },
108
+ "count": { "type": "integer", "minimum": 0 }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ },
115
+ "valuesOmitted": { "type": "integer", "minimum": 0 }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ },
121
+ "planning": {
122
+ "description": "Incident planning; null when it did not run for this change.",
123
+ "oneOf": [
124
+ { "type": "null" },
125
+ {
126
+ "type": "object",
127
+ "required": ["groups", "unresolved", "ideas"],
128
+ "properties": {
129
+ "groups": {
130
+ "description": "Behavior groups of the change, each with the failure patterns selected for it.",
131
+ "type": "array",
132
+ "items": {
133
+ "type": "object",
134
+ "required": ["id", "changeKeys", "patterns"],
135
+ "properties": {
136
+ "id": { "type": "string" },
137
+ "changeKeys": { "type": "array", "items": { "type": "string" } },
138
+ "patterns": {
139
+ "type": "array",
140
+ "items": { "type": "object", "required": ["entryId", "mechanism", "symptom"],
141
+ "properties": { "entryId": { "type": "string" }, "mechanism": { "type": "string" }, "symptom": { "type": "string" } } }
142
+ }
143
+ }
144
+ }
145
+ },
146
+ "unresolved": {
147
+ "description": "Changes no behavior group holds, and why.",
148
+ "type": "array",
149
+ "items": { "type": "object", "required": ["changeKey", "reason"], "properties": { "changeKey": { "type": "string" }, "reason": { "type": "string" } } }
150
+ },
151
+ "ideas": {
152
+ "description": "Haystack's own ideas: the selected situations, in plan order (at most 64). Every crawl explores them; pass the ones you want first, and your own, to `haystack verify --idea`.",
153
+ "type": "array",
154
+ "maxItems": 64,
155
+ "items": {
156
+ "type": "object",
157
+ "required": ["id", "groupId", "pattern", "why", "failure", "setup", "exercise", "watch", "start"],
158
+ "properties": {
159
+ "id": { "type": "string" },
160
+ "groupId": { "type": "string" },
161
+ "pattern": { "type": "string", "description": "The failure pattern's library id." },
162
+ "why": { "type": "string", "description": "How that pattern could arise in this code." },
163
+ "failure": { "type": "string", "description": "What would go wrong." },
164
+ "setup": { "type": "array", "items": { "type": "string" }, "description": "What must be set up or true first." },
165
+ "exercise": { "type": "string", "description": "What to do." },
166
+ "watch": { "type": "string", "description": "What to watch." },
167
+ "start": { "type": "string", "description": "Where it starts: the start screen, or a prepared start." }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ ]
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "properties": {
13
13
  "schema_version": {
14
- "const": "1.0.0"
14
+ "const": "1.0.1"
15
15
  },
16
16
  "facts": {
17
17
  "type": "object",
@@ -52,6 +52,13 @@
52
52
  "additionalProperties": {
53
53
  "type": "string"
54
54
  }
55
+ },
56
+ "notes": {
57
+ "description": "What the repository's coding agent said about the app (APP-ONBOARDING-V1 rule 7, amendment 18), present once written with `haystack init --notes`; empty where it said nothing.",
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "required": ["run", "signIn", "workflow"],
61
+ "properties": { "run": { "type": "string" }, "signIn": { "type": "string" }, "workflow": { "type": "string" } }
55
62
  }
56
63
  }
57
64
  },
@@ -17,7 +17,7 @@
17
17
  }
18
18
  ]
19
19
  },
20
- "schema_version": { "const": "1.0.2" },
20
+ "schema_version": { "const": "1.0.4" },
21
21
  "change": {
22
22
  "description": "Schema 1.0.2: what this verify checked: the capture's base commit (where HEAD meets origin's default branch), its work commit (committed and uncommitted changes), and the files that differ between them.",
23
23
  "type": "object",
@@ -452,6 +452,27 @@
452
452
  "gaps": { "type": "array", "maxItems": 200, "items": { "type": "string" } }
453
453
  }
454
454
  },
455
+ "ideas": {
456
+ "description": "CRAWL-V1 amendment 17, only when `--idea` or `--ideas-file` gave ideas: one entry per idea in the order given. 'covered': an independent review saw it done in both builds; 'blocked': it could not be done in this copy (reason says why); 'unfinished': the crawl's time ended first. findingIds names the findings found while exploring it.",
457
+ "type": "array",
458
+ "maxItems": 64,
459
+ "items": {
460
+ "type": "object",
461
+ "required": ["id", "text", "state", "reason", "findingIds"],
462
+ "properties": {
463
+ "id": { "type": "string", "pattern": "^idea-[1-9][0-9]*$" },
464
+ "text": { "type": "string" },
465
+ "state": { "enum": ["covered", "blocked", "unfinished"] },
466
+ "reason": { "type": "string" },
467
+ "findingIds": { "$ref": "#/$defs/strings" }
468
+ }
469
+ }
470
+ },
471
+ "brief": {
472
+ "description": "CRAWL-V1 amendment 18: what Haystack knew of the change before exploring it (see `haystack schema pre-verify`, $defs.brief); absent when the run ended before its prepare.",
473
+ "type": "object",
474
+ "required": ["functions", "leftOut", "planning"]
475
+ },
455
476
  "notFinishedInTime": { "$ref": "#/$defs/count" },
456
477
  "error": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/error" }] }
457
478
  }