@haystackeditor/cli 0.25.1 → 0.27.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.
Files changed (109) hide show
  1. package/README.md +39 -463
  2. package/dist/capture/app-config.js +30 -6
  3. package/dist/commands/capture-brief.js +45 -35
  4. package/dist/commands/capture-contract.js +4 -4
  5. package/dist/commands/crawl-report.js +196 -0
  6. package/dist/commands/db-profile-upload.js +3 -3
  7. package/dist/commands/feedback.js +66 -0
  8. package/dist/commands/init-telemetry.js +55 -9
  9. package/dist/commands/init.js +8 -3
  10. package/dist/commands/lockfile-pin.js +307 -0
  11. package/dist/commands/telemetry-token.js +3 -3
  12. package/dist/commands/tokens.js +4 -4
  13. package/dist/commands/verify-explore.js +3 -3
  14. package/dist/commands/verify-history.js +2 -2
  15. package/dist/commands/verify-onboarding.js +13 -16
  16. package/dist/commands/verify-precompute.js +3 -4
  17. package/dist/commands/verify.js +31 -118
  18. package/dist/index.js +55 -968
  19. package/dist/schema.js +4 -10
  20. package/dist/utils/haystack-api.js +8 -36
  21. package/package.json +1 -5
  22. package/schemas/feedback.v1.json +13 -0
  23. package/schemas/pre-verify.v2.json +240 -0
  24. package/schemas/verify-raw.v1.json +1132 -0
  25. package/schemas/verify.v2.json +655 -0
  26. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  27. package/dist/assets/hooks/agent-context/format.ts +0 -100
  28. package/dist/assets/hooks/agent-context/index.ts +0 -41
  29. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  30. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  31. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  32. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  33. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  34. package/dist/assets/hooks/agent-context/types.ts +0 -58
  35. package/dist/assets/hooks/llm-rules-template.md +0 -59
  36. package/dist/assets/hooks/package-lock.json +0 -598
  37. package/dist/assets/hooks/package.json +0 -12
  38. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  39. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  40. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  41. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  42. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  43. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  44. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  45. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  46. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  47. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  48. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  49. package/dist/assets/skills/map-your-system.md +0 -143
  50. package/dist/assets/skills/submit.md +0 -200
  51. package/dist/commands/ask.js +0 -20
  52. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  53. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  54. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  55. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  56. package/dist/commands/cloud-verifier-materialization.js +0 -704
  57. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  58. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  59. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  60. package/dist/commands/cloud-verifier-universe.js +0 -10178
  61. package/dist/commands/config.js +0 -549
  62. package/dist/commands/design-verify.js +0 -311
  63. package/dist/commands/dismiss.js +0 -159
  64. package/dist/commands/hooks.js +0 -226
  65. package/dist/commands/inbox.js +0 -137
  66. package/dist/commands/mcp.js +0 -201
  67. package/dist/commands/policy.js +0 -371
  68. package/dist/commands/pr-status.js +0 -207
  69. package/dist/commands/pr.js +0 -105
  70. package/dist/commands/prepare-universe-review.js +0 -1092
  71. package/dist/commands/production-source-deny-policy.js +0 -100
  72. package/dist/commands/request-review.js +0 -74
  73. package/dist/commands/review.js +0 -191
  74. package/dist/commands/rules.js +0 -98
  75. package/dist/commands/scaffold-provisional-universe.js +0 -806
  76. package/dist/commands/setup.js +0 -1170
  77. package/dist/commands/skills.js +0 -447
  78. package/dist/commands/status.js +0 -35
  79. package/dist/commands/submit.js +0 -745
  80. package/dist/commands/system-map.js +0 -228
  81. package/dist/commands/triage.js +0 -598
  82. package/dist/commands/webhooks.js +0 -241
  83. package/dist/states.js +0 -46
  84. package/dist/tools/detect.js +0 -832
  85. package/dist/triage/astra.js +0 -202
  86. package/dist/triage/prompts.js +0 -188
  87. package/dist/triage/runner.js +0 -200
  88. package/dist/triage/types.js +0 -7
  89. package/dist/types.js +0 -326
  90. package/dist/utils/action-output.js +0 -26
  91. package/dist/utils/analysis-api.js +0 -416
  92. package/dist/utils/config.js +0 -54
  93. package/dist/utils/design-verifier-api.js +0 -294
  94. package/dist/utils/design-verifier-history.js +0 -79
  95. package/dist/utils/design-verifier-result.js +0 -424
  96. package/dist/utils/github-api.js +0 -324
  97. package/dist/utils/pending-state.js +0 -86
  98. package/dist/utils/pr-ref.js +0 -56
  99. package/dist/utils/prompter.js +0 -328
  100. package/schemas/action.v1.json +0 -22
  101. package/schemas/ask.v1.json +0 -40
  102. package/schemas/inbox.v1.json +0 -27
  103. package/schemas/pr-status.v1.json +0 -61
  104. package/schemas/pr.v1.json +0 -97
  105. package/schemas/pr.v3.json +0 -45
  106. package/schemas/setup.v1.json +0 -75
  107. package/schemas/submit.v1.json +0 -90
  108. package/schemas/triage.v1.json +0 -103
  109. package/schemas/triage.v2.json +0 -64
@@ -1,143 +0,0 @@
1
- # Map Your System for Haystack QA
2
-
3
- Create `.haystack/system.yml` — a short file of facts about how this
4
- repository's system works, so Haystack QA can run it.
5
-
6
- ## Why this file exists
7
-
8
- When Haystack reviews a risky PR, it doesn't just read the diff — it runs the
9
- affected code in a sandbox and captures evidence. Haystack decides **what** to
10
- test from each PR on its own. What it can't always infer is **how your system
11
- works**: how to install and build it, how services boot, how to know they're
12
- ready, how to seed data, how to act as a logged-in test user. That is what
13
- this file provides. Every fact you add converts risks Haystack identified but
14
- couldn't execute into checks that actually run.
15
-
16
- ## Hard rules
17
-
18
- 1. **Facts only — never test selection.** The file describes how the system
19
- works. Keys like `scenarios`, `tests`, `skip`, or `focus` are rejected;
20
- Haystack alone decides what to test.
21
- 2. **Only verified facts.** Run every command yourself before declaring it.
22
- A wrong fact is worse than a missing one — Haystack tracks which declared
23
- facts fail and will flag them on PRs.
24
- 3. **No secrets.** Reference environment variable *names* if needed, never
25
- values. Anything that looks like a credential is rejected.
26
- 4. **Personas are seeded FAKE users** reachable through dev/test-only login
27
- paths. Never real accounts, never customer data. If a dev login helper
28
- exists, it must be disabled in production builds.
29
- 5. **Small and true beats big and aspirational.** Ten verified lines are more
30
- useful than sixty guesses. Omit anything you could not verify.
31
-
32
- ## Steps
33
-
34
- ### 1. Inspect the repository
35
-
36
- Determine, with evidence from the tree (lockfiles, manifests, scripts,
37
- docker-compose, CI config, READMEs):
38
-
39
- - package manager, install command, and any required build/codegen steps
40
- - each runnable service: start command, port, and how "ready" is observable
41
- (health endpoint, log line, or a command that exits 0)
42
- - how to seed and reset local/test data
43
- - how existing tests authenticate — is there a seeded test user or dev login?
44
- - the test runner and any test helpers/factories generated tests should reuse
45
- - external services the code calls, and how development runs without hitting
46
- them for real (mock fixture, log driver, sandbox mode)
47
- - where runtime logs are written
48
-
49
- ### 2. Verify each fact by running it
50
-
51
- From a clean state where possible: run the install, run the build, boot each
52
- service and confirm its readiness signal, run the seed command, run the login
53
- helper. Record the exact commands that worked. Drop anything you could not
54
- make work — do not declare it with a caveat.
55
-
56
- ### 3. Ask the developer only what you cannot determine
57
-
58
- Typical questions worth asking a human: "Is there a dev-only way to log in as
59
- a test user?", "Which services must be running for the API to work?", "Is
60
- there a canonical seed script?". Keep it to the few facts you genuinely
61
- cannot infer or verify.
62
-
63
- ### 4. Write `.haystack/system.yml`
64
-
65
- ```yaml
66
- version: 1
67
-
68
- environment:
69
- setup:
70
- - run: "pnpm install --frozen-lockfile"
71
- - run: "pnpm codegen"
72
- rationale: "generated types are imported throughout"
73
- toolchain: { node: "20", pnpm: "10" }
74
-
75
- services:
76
- api:
77
- root: "apps/api" # optional
78
- boot: "pnpm dev"
79
- ready: { http: "http://localhost:8080/health" } # or { log_pattern: "listening" } or { command: "pg_isready" }
80
- env: { APP_ENV: "test" }
81
- depends_on: [postgres]
82
-
83
- data:
84
- seed: "pnpm db:seed --profile=qa"
85
- reset: "pnpm db:reset"
86
- personas:
87
- admin:
88
- login: "pnpm test:login admin"
89
- yields: "prints an Authorization header on stdout"
90
-
91
- observability:
92
- logs: ["apps/api/log/dev.log"]
93
- inspect_db: "pnpm db:console --readonly"
94
-
95
- external_services:
96
- stripe:
97
- offline: "repo ships a stripe-mock fixture used by tests"
98
- must_not_touch_live: true
99
-
100
- testing:
101
- runner: "pnpm vitest"
102
- probe_helpers: "tests/helpers/ has request builders and DB factories"
103
-
104
- constraints:
105
- - "full build takes ~6 min; apps/api boots standalone without the web build"
106
- ```
107
-
108
- Every section is optional — include only what you verified. All commands run
109
- from the repo root unless a `cwd`/`root` says otherwise.
110
-
111
- ### 5. Validate before opening the PR
112
-
113
- Run the validator — it applies the same rules Haystack QA applies:
114
-
115
- ```bash
116
- npx @haystackeditor/cli system-map validate
117
- ```
118
-
119
- Fix every reported error, then confirm the things the validator cannot check:
120
-
121
- - [ ] every `run`/`boot`/`login`/`seed` command was executed and worked
122
- - [ ] every service has a `ready` signal you observed
123
- - [ ] personas are seeded fake users via dev/test-only paths
124
-
125
- Haystack re-validates the file on its next QA run and reports precise errors
126
- if something is wrong.
127
-
128
- ### 6. Open a small, reviewable PR
129
-
130
- Add only `.haystack/system.yml` (plus a dev login helper if one was genuinely
131
- needed — dev/test-gated, with a test proving it's off in production). In the
132
- PR description, list which facts you verified by running them and which came
133
- from the developer's answers.
134
-
135
- ## What happens next
136
-
137
- Haystack treats each entry as a claim. When a QA run executes a declared
138
- command successfully, the fact is marked **verified** with the run's receipt;
139
- if it fails at a newer commit it is marked **broken**, and Haystack will ask
140
- about it on a PR instead of trusting it. When QA hits something the map
141
- doesn't cover, it asks at most a couple of targeted questions on the affected
142
- PR, each with a ready-to-paste snippet — answering them grows this file over
143
- time.
@@ -1,200 +0,0 @@
1
- # Submit PR via Haystack
2
-
3
- **Your job**: Submit your changes as a PR using `haystack submit` instead of `gh pr create` or GitHub API.
4
-
5
- ---
6
-
7
- ## Why Use Haystack Submit?
8
-
9
- When you create PRs through Haystack, the CLI keeps PR creation inside Haystack's analysis workflow:
10
-
11
- 1. **Runs pre-PR triage** on your current branch
12
- 2. **Pushes that branch** to origin
13
- 3. **Creates the PR** on GitHub
14
- 4. **Waits for Haystack analysis** by default
15
- 5. **Applies auto-merge** only if the repo has it enabled in `.haystack.json`
16
-
17
- ---
18
-
19
- ## Command Reference
20
-
21
- ```bash
22
- # Default: Submit current branch and create the PR
23
- haystack submit
24
-
25
- # Optional alpha auto-fix for straightforward mechanical issues
26
- haystack submit --auto-fix
27
-
28
- # Request Haystack review (for complex/risky changes)
29
- haystack submit --review
30
-
31
- # With custom title
32
- haystack submit --title "Fix authentication bug"
33
-
34
- # Target different base branch
35
- haystack submit --base develop
36
-
37
- # With markdown PR body (write to file first to preserve formatting)
38
- cat > /tmp/pr-body.md << 'EOF'
39
- ## Summary
40
- - Fixed authentication bug in session handling
41
- - Added integration tests for token refresh
42
-
43
- ## Test plan
44
- - [x] Auth flow works end-to-end
45
- EOF
46
- haystack submit --body-file /tmp/pr-body.md
47
- ```
48
-
49
- > **Tip**: Always use `--body-file` instead of `--body` when the description contains markdown.
50
- > The `--body` flag is a single CLI argument that mangles newlines and special characters (`#`, `*`, etc.).
51
-
52
- ---
53
-
54
- ## When to Use Each Mode
55
-
56
- ### Default Submit
57
-
58
- Use for straightforward changes that don't need explicit human review:
59
-
60
- - Bug fixes with clear scope
61
- - Small features with tests
62
- - Refactoring with no behavior change
63
- - Documentation updates
64
- - Dependency updates
65
-
66
- ```bash
67
- haystack submit
68
- ```
69
-
70
- If the repo has `preferences.auto_merge` enabled in `.haystack.json`, the PR will also enter the auto-merge queue.
71
-
72
- ### Auto-Fix (Alpha, Discouraged)
73
-
74
- > **Do NOT default to `--auto-fix`.** It is an alpha feature.
75
- > Use it only when the user explicitly wants to opt in, or when they are
76
- > comfortable using the sandbox fixer for clearly straightforward mechanical issues.
77
-
78
- Use `--auto-fix` only for issues like:
79
-
80
- - Missing null checks
81
- - Obvious typos or lint fixes
82
- - Simple deterministic bugs with one clear fix
83
-
84
- ```bash
85
- haystack submit --auto-fix
86
- ```
87
-
88
- Repos that want auto-fix on by default for every `haystack submit` can opt
89
- in via `.haystack.json`:
90
-
91
- ```json
92
- {
93
- "preferences": {
94
- "auto_fix": true
95
- }
96
- }
97
- ```
98
-
99
- When set, `haystack submit` applies the `haystack:auto-fix` label without
100
- needing the explicit flag. Auto-fix is still alpha — use the repo-level
101
- opt-in only when the team is comfortable with the sandbox fixer running on
102
- mechanical findings. `haystack submit --no-auto-fix` overrides the repo
103
- default for a single submission.
104
-
105
- ### Review Mode
106
-
107
- > **Do NOT use `--review` unless the user explicitly asks for human review.**
108
- > Plain `haystack submit` already runs full Haystack analysis. Using `--review`
109
- > blocks auto-merge when auto-merge is enabled, which delays merging unnecessarily.
110
-
111
- Use `--review` only when the user specifically requests human review:
112
-
113
- - User says "get someone to review this" or "needs human eyes"
114
- - User explicitly passes `--review` in their instructions
115
-
116
- ```bash
117
- haystack submit --review
118
- ```
119
-
120
- ---
121
-
122
- ## Workflow
123
-
124
- 1. **Make your changes** on a feature branch
125
- 2. **Commit** your changes: `git add . && git commit -m "..."`
126
- 3. **Submit** via Haystack: `haystack submit`
127
-
128
- Haystack will:
129
- - Push your current branch to origin
130
- - Create the PR with appropriate labels
131
- - Trigger analysis automatically
132
-
133
- ---
134
-
135
- ## Prerequisites
136
-
137
- Before using `haystack submit`, ensure you're logged in:
138
-
139
- ```bash
140
- haystack login
141
- ```
142
-
143
- This uses GitHub device flow - no tokens to copy/paste.
144
-
145
- ---
146
-
147
- ## Important Notes
148
-
149
- - **Always use `haystack submit`** instead of `gh pr create` when working in Haystack-enabled repos
150
- - Prefer plain `haystack submit` by default; use `--auto-fix` only as an explicit alpha opt-in for straightforward mechanical issues
151
- - The command uses your current branch as the PR head
152
- - Labels are applied automatically based on your chosen mode
153
- - You don't need to push manually - the command handles it
154
-
155
- ## Close the Loop After Submitting
156
-
157
- After `haystack submit` creates the PR, **run `haystack triage <pr>` before ending your turn.**
158
- It blocks until Haystack's full analysis completes (waking instantly on the completion event),
159
- then prints the rating, findings, and a per-finding `agentFixPrompt`.
160
-
161
- - **Findings marked auto-fixing**: leave them alone — Haystack's fixer is already on them
162
- - **Actionable findings**: address them (the `agentFixPrompt` is written for you), commit, and
163
- push — analysis re-runs on the new commit
164
- - **Good to merge**: report the rating to your user and stop
165
-
166
- Don't end the session right after submit without checking — the verdict usually lands within a
167
- few minutes, and acting on it immediately is much cheaper than a human bouncing the PR back.
168
-
169
- ---
170
-
171
- ## Example Session
172
-
173
- ```
174
- Agent: I've fixed the authentication bug. Let me submit this as a PR.
175
-
176
- $ haystack submit --title "Fix session expiry handling"
177
-
178
- Haystack Submit
179
-
180
- ✓ Authenticated
181
- ✓ Repository: acme/webapp
182
- Current branch: fix-auth
183
- Base branch: main
184
- ✓ Branch pushed
185
- ✓ Pull request created: #142
186
- ✓ Auto-merge enabled for this PR
187
-
188
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
189
-
190
- Pull Request Created!
191
-
192
- URL: https://github.com/acme/webapp/pull/142
193
- Title: Fix session expiry handling
194
- Branch: fix-auth → main
195
- Merge: Auto-merge if safe
196
-
197
- Haystack will analyze this PR and:
198
- • Auto-merge if approved
199
- • Route for review if issues found
200
- ```
@@ -1,20 +0,0 @@
1
- import chalk from 'chalk';
2
- import { loadToken } from '../utils/auth.js';
3
- import { postAskHaystack } from '../utils/haystack-api.js';
4
- import { parsePrRef } from '../utils/pr-ref.js';
5
- export async function fetchAskPayload(ref, question, options) {
6
- const pr = parsePrRef(ref, 'ask');
7
- const token = await loadToken({ owner: pr.owner, repo: pr.repo });
8
- if (!token)
9
- throw new Error('Not authenticated. Run `haystack login` first.');
10
- return postAskHaystack(pr.owner, pr.repo, pr.prNumber, question, options.session, token);
11
- }
12
- export async function askHaystackCommand(ref, question, options) {
13
- const response = await fetchAskPayload(ref, question, options);
14
- if (options.json) {
15
- console.log(JSON.stringify(response, null, 2));
16
- return;
17
- }
18
- console.log(`\n${response.answer}\n`);
19
- console.log(chalk.dim(`${response.evidence.length} source${response.evidence.length === 1 ? '' : 's'} consulted · session ${response.session_id}\n`));
20
- }
@@ -1,218 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { lstatSync, readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import chalk from 'chalk';
5
- import { parseDocument } from 'yaml';
6
- import { findGitRoot } from '../utils/hooks.js';
7
- // Keep behavior validation on the same bounded artifact envelope as universe
8
- // validation. Large maps must not need a lossy reference projection merely to
9
- // validate their exact interface and evidence joins.
10
- export const MAX_BEHAVIOR_ARTIFACT_BYTES = 32 * 1024 * 1024;
11
- const GIT_COMMIT = /^[0-9a-f]{40}$/;
12
- const STABLE_UNIVERSE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
13
- const REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
14
- const RUNTIME_ID = /^(?:action|symbol|boundary)-[0-9a-f]{24}$/;
15
- const BOUNDARY_KINDS = new Set(['response', 'state', 'event', 'external', 'ui', 'cli', 'job']);
16
- const TRIGGER_KINDS = new Set(['http', 'event', 'job', 'cli', 'ui']);
17
- function stableIdentity(prefix, parts) {
18
- const framed = parts.map(part => `${Buffer.byteLength(part, 'utf8')}:${part}`).join('|');
19
- return `${prefix}-${createHash('sha256').update(framed).digest('hex').slice(0, 24)}`;
20
- }
21
- function normalizeRepository(repository) {
22
- if (!REPOSITORY.test(repository))
23
- throw new Error('must be a canonical owner/repository identity');
24
- return repository.toLowerCase();
25
- }
26
- function normalizePath(sourcePath) {
27
- const normalized = sourcePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
28
- if (!normalized || normalized.split('/').some(part => !part || part === '..')) {
29
- throw new Error('must be a normalized repository-relative source path');
30
- }
31
- return normalized;
32
- }
33
- export function derivedBehaviorActionId(repository, trigger) {
34
- let identity;
35
- if (trigger?.kind === 'http') {
36
- if (typeof trigger.method !== 'string' || typeof trigger.route_template !== 'string') {
37
- throw new Error('HTTP trigger requires method and route_template');
38
- }
39
- identity = `${trigger.method.toUpperCase()} ${trigger.route_template}`;
40
- }
41
- else if (trigger?.kind === 'event') {
42
- if (typeof trigger.channel_identity !== 'string' || typeof trigger.event_type !== 'string') {
43
- throw new Error('event trigger requires channel_identity and event_type');
44
- }
45
- identity = `${trigger.channel_identity}\0${trigger.event_type}`;
46
- }
47
- else if (trigger?.kind === 'job')
48
- identity = trigger.job_identity;
49
- else if (trigger?.kind === 'cli')
50
- identity = trigger.command_identity;
51
- else if (trigger?.kind === 'ui')
52
- identity = trigger.action_identity;
53
- else
54
- throw new Error('trigger kind must be http, event, job, cli, or ui');
55
- if (typeof identity !== 'string' || !identity.trim())
56
- throw new Error('trigger identity must be non-empty');
57
- return stableIdentity('action', [normalizeRepository(repository), trigger.kind, identity]);
58
- }
59
- export function derivedBehaviorSymbolId(repository, sourcePath, qualifiedName) {
60
- if (typeof qualifiedName !== 'string' || !qualifiedName.trim())
61
- throw new Error('qualified_name must be non-empty');
62
- return stableIdentity('symbol', [normalizeRepository(repository), normalizePath(sourcePath), qualifiedName.trim()]);
63
- }
64
- export function derivedBehaviorBoundaryId(repository, kind, identityKey) {
65
- if (!BOUNDARY_KINDS.has(kind))
66
- throw new Error('boundary kind is unsupported');
67
- if (typeof identityKey !== 'string' || !identityKey.trim())
68
- throw new Error('identity_key must be non-empty');
69
- return stableIdentity('boundary', [normalizeRepository(repository), kind, identityKey.trim()]);
70
- }
71
- function parseYaml(text, name) {
72
- const document = parseDocument(text, { prettyErrors: false });
73
- if (document.errors.length)
74
- throw new Error(`${name} is malformed YAML: ${document.errors[0].message}`);
75
- const value = document.toJS({ maxAliasCount: 50 });
76
- if (!value || typeof value !== 'object' || Array.isArray(value))
77
- throw new Error(`${name} must contain a mapping`);
78
- return value;
79
- }
80
- export function validateBehaviorArtifact(value, references) {
81
- const issues = [];
82
- const fail = (path, message) => issues.push({ path, message });
83
- if (value?.version !== 1)
84
- fail('version', 'must equal 1');
85
- if (value?.schema_version !== 'haystack-behavior-map-v1') {
86
- fail('schema_version', 'must equal haystack-behavior-map-v1');
87
- }
88
- let repository = '';
89
- try {
90
- repository = normalizeRepository(value?.repository);
91
- }
92
- catch (error) {
93
- fail('repository', error.message);
94
- }
95
- if (!GIT_COMMIT.test(value?.source_commit || ''))
96
- fail('source_commit', 'must be an exact Git commit');
97
- const universeCommit = references.universe?.map_revision?.source_commit;
98
- if (value?.source_commit !== universeCommit)
99
- fail('source_commit', 'must exactly match universe.yml map_revision.source_commit');
100
- const interfaceIds = new Set((Array.isArray(references.universe?.interfaces)
101
- ? references.universe.interfaces : []).map((item) => item?.id));
102
- const evidenceIds = new Set((Array.isArray(references.evidence?.evidence)
103
- ? references.evidence.evidence : []).map((item) => item?.id));
104
- if (!Array.isArray(value?.actions) || value.actions.length === 0) {
105
- fail('actions', 'must contain at least one meaningful customer action');
106
- return { version: 'haystack-behavior-validation-v1', status: 'invalid', issues, summary: { actions: 0, boundaries: 0 } };
107
- }
108
- const actionIds = new Set();
109
- const triggerIds = new Set();
110
- let boundaryCount = 0;
111
- for (const [index, action] of value.actions.entries()) {
112
- const path = `actions[${index}]`;
113
- if (!action || typeof action !== 'object' || Array.isArray(action)) {
114
- fail(path, 'must be a mapping');
115
- continue;
116
- }
117
- let expectedAction = '';
118
- try {
119
- expectedAction = derivedBehaviorActionId(repository || value.repository, action.trigger);
120
- }
121
- catch (error) {
122
- fail(`${path}.trigger`, error.message);
123
- }
124
- if (!RUNTIME_ID.test(action.id || '') || action.id !== expectedAction)
125
- fail(`${path}.id`, `must equal derived exact id ${expectedAction}`);
126
- if (actionIds.has(action.id))
127
- fail(`${path}.id`, 'must be unique');
128
- actionIds.add(action.id);
129
- if (!TRIGGER_KINDS.has(action.trigger?.kind))
130
- fail(`${path}.trigger.kind`, 'is unsupported');
131
- if (triggerIds.has(expectedAction))
132
- fail(`${path}.trigger`, 'duplicates another structured trigger');
133
- triggerIds.add(expectedAction);
134
- if (!STABLE_UNIVERSE_ID.test(action.interface_id || '') || !interfaceIds.has(action.interface_id)) {
135
- fail(`${path}.interface_id`, 'must reference an exact universe.yml interface id');
136
- }
137
- let expectedSymbol = '';
138
- try {
139
- expectedSymbol = derivedBehaviorSymbolId(repository || value.repository, action.handler?.source_path, action.handler?.qualified_name);
140
- }
141
- catch (error) {
142
- fail(`${path}.handler`, error.message);
143
- }
144
- if (action.handler?.symbol_id !== expectedSymbol)
145
- fail(`${path}.handler.symbol_id`, `must equal derived exact id ${expectedSymbol}`);
146
- if (!Array.isArray(action.observable_boundaries) || action.observable_boundaries.length === 0) {
147
- fail(`${path}.observable_boundaries`, 'must contain a customer-relevant boundary');
148
- }
149
- else {
150
- const boundaryIds = new Set();
151
- for (const [boundaryIndex, boundary] of action.observable_boundaries.entries()) {
152
- boundaryCount += 1;
153
- const boundaryPath = `${path}.observable_boundaries[${boundaryIndex}]`;
154
- let expectedBoundary = '';
155
- try {
156
- expectedBoundary = derivedBehaviorBoundaryId(repository || value.repository, boundary?.kind, boundary?.identity_key);
157
- }
158
- catch (error) {
159
- fail(boundaryPath, error.message);
160
- }
161
- if (boundary?.id !== expectedBoundary)
162
- fail(`${boundaryPath}.id`, `must equal derived exact id ${expectedBoundary}`);
163
- if (boundaryIds.has(boundary?.id))
164
- fail(`${boundaryPath}.id`, 'must be unique in the action');
165
- boundaryIds.add(boundary?.id);
166
- }
167
- }
168
- if (!Array.isArray(action.evidence_ids) || action.evidence_ids.length === 0) {
169
- fail(`${path}.evidence_ids`, 'must contain exact repository evidence');
170
- }
171
- else {
172
- for (const evidenceId of action.evidence_ids) {
173
- if (!evidenceIds.has(evidenceId))
174
- fail(`${path}.evidence_ids`, `references unknown evidence id ${String(evidenceId)}`);
175
- }
176
- }
177
- }
178
- return {
179
- version: 'haystack-behavior-validation-v1',
180
- status: issues.length ? 'invalid' : 'valid',
181
- issues,
182
- summary: { actions: value.actions.length, boundaries: boundaryCount },
183
- };
184
- }
185
- function readArtifact(root, name) {
186
- const path = join(root, '.haystack', 'cloud-verifier', name);
187
- const metadata = lstatSync(path, { throwIfNoEntry: false });
188
- if (!metadata || !metadata.isFile() || metadata.isSymbolicLink())
189
- throw new Error(`${name} must be a regular non-symlink file`);
190
- if (metadata.size > MAX_BEHAVIOR_ARTIFACT_BYTES) {
191
- throw new Error(`${name} exceeds ${MAX_BEHAVIOR_ARTIFACT_BYTES} bytes`);
192
- }
193
- return readFileSync(path, 'utf8');
194
- }
195
- export function resolveBehaviorArtifactRoot(gitRoot, currentDirectory = process.cwd()) {
196
- return gitRoot ?? currentDirectory;
197
- }
198
- export async function cloudVerifierBehaviorsValidateCommand(options) {
199
- // Source-only evaluation archives deliberately omit Git metadata. The
200
- // behavior validator only reads verifier artifacts, so current-directory
201
- // fallback is both sufficient and consistent with validate-universe.
202
- const root = resolveBehaviorArtifactRoot(findGitRoot());
203
- const value = parseYaml(readArtifact(root, 'behaviors.yml'), 'behaviors.yml');
204
- const universe = parseYaml(readArtifact(root, 'universe.yml'), 'universe.yml');
205
- const evidence = parseYaml(readArtifact(root, 'evidence.yml'), 'evidence.yml');
206
- const result = validateBehaviorArtifact(value, { universe, evidence });
207
- if (options.json)
208
- console.log(JSON.stringify(result, null, 2));
209
- else if (result.status === 'valid') {
210
- console.log(chalk.green(`Behavior map valid: ${result.summary.actions} actions, ${result.summary.boundaries} boundaries`));
211
- }
212
- else {
213
- for (const issue of result.issues)
214
- console.error(chalk.red(`behaviors.yml:${issue.path}: ${issue.message}`));
215
- }
216
- if (result.status !== 'valid')
217
- process.exitCode = 1;
218
- }