@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,806 +0,0 @@
1
- import { closeSync, constants as fsConstants, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
2
- import { createHash } from 'node:crypto';
3
- import { execFileSync } from 'node:child_process';
4
- import { basename, dirname, isAbsolute, join, normalize, parse as parsePath, relative, resolve, sep, } from 'node:path';
5
- import { stringify as stringifyYaml } from 'yaml';
6
- import { z } from 'zod';
7
- import { findGitRoot } from '../utils/hooks.js';
8
- import { VALIDATOR_CONTRACT_REVISION, validateUniverseArtifactTexts, } from './cloud-verifier-universe.js';
9
- const STABLE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10
- const SOURCE_COMMIT = /^[0-9a-f]{40}$/;
11
- const ARTIFACT_NAMES = ['universe.yml', 'evidence.yml', 'review.yml'];
12
- const PRIMARY_CALL_LIMIT = 48;
13
- const PRIMARY_ELAPSED_LIMIT_SECONDS = 900;
14
- const AUTHORITY_CALL_LIMIT = 6;
15
- const AUTHORITY_ELAPSED_LIMIT_SECONDS = 60;
16
- export const TOPOLOGY_PRESENCE_SURFACE_KINDS = [
17
- 'runtime-entrypoints',
18
- 'package-workspace-manifests',
19
- 'container-descriptors',
20
- 'deployment-descriptors',
21
- 'structured-capability-indexes',
22
- ];
23
- export const PROVISIONAL_UNIVERSE_INPUT_MAX_BYTES = 256 * 1024;
24
- export const PROVISIONAL_ACCESS_REQUEST_FIELDS = [
25
- 'selected-application-id-and-environment-id',
26
- 'approved-replica-destination-policy-and-approval-ref',
27
- 'bounded-resource-kinds-opaque-catalog-handles-and-dependency-edges',
28
- 'artifact-runtime-and-non-secret-config-opaque-catalog-refs',
29
- 'external-integration-kinds-and-secret-reference-names',
30
- 'replica-capabilities-strategies-consistency-freshness-latency-and-cost-bounds',
31
- 'replica-only-golden-descriptor-and-lease-reference-contract',
32
- ];
33
- const UNSAFE_VALUE_PATTERNS = [
34
- [/[a-z][a-z0-9+.-]*:\/\//i, 'raw URLs are forbidden'],
35
- [/(?:jdbc|postgres(?:ql)?|mysql|mariadb|redis|rediss|amqp|amqps|mongodb(?:\+srv)?|sqlserver):/i, 'connection strings are forbidden'],
36
- [/\barn:(?:aws|aws-cn|aws-us-gov):/i, 'provider ARNs are forbidden'],
37
- [/(?:^|[^a-z0-9])(?:acl|ami|eipalloc|eni|fs|i|igw|nat|rtb|sg|snap|subnet|vol|vpc|vpce)-[0-9a-f]{8,17}(?=$|[^a-z0-9])/i, 'raw provider resource IDs are forbidden'],
38
- [/(?:^|[/])subscriptions\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?:[/]|$)/i, 'raw Azure resource IDs are forbidden'],
39
- [/(?:^|[^a-z0-9])projects\/[^/\s]+\/(?:zones|regions|locations|instances|clusters|databases|buckets)(?:[/]|$)/i, 'raw Google Cloud resource IDs are forbidden'],
40
- [/\bocid1\.[a-z0-9.-]+\.[a-z0-9.-]*[a-z0-9]\b/i, 'raw OCI resource IDs are forbidden'],
41
- [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/, 'credential values are forbidden'],
42
- [/\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk_(?:live|test)_[A-Za-z0-9]+)\b/, 'credential values are forbidden'],
43
- [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, 'credential values are forbidden'],
44
- [/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/, 'credential values are forbidden'],
45
- [/\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/i, 'credential values are forbidden'],
46
- [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, 'private key values are forbidden'],
47
- [/(?:password|secret|token|api[_-]?key)\s*[:=]\s*[^\s,;]+/i, 'secret values are forbidden'],
48
- ];
49
- function sanitizedStringSchema(schema, label) {
50
- return schema.superRefine((value, context) => {
51
- if (value.includes('\0')) {
52
- context.addIssue({ code: 'custom', message: `${label} must not contain NUL` });
53
- }
54
- for (const [pattern, message] of UNSAFE_VALUE_PATTERNS) {
55
- if (pattern.test(value)) {
56
- context.addIssue({ code: 'custom', message: `${label}: ${message}` });
57
- }
58
- }
59
- });
60
- }
61
- function safeStringSchema(label) {
62
- return sanitizedStringSchema(z.string().min(1, `${label} must not be empty`), label);
63
- }
64
- const stableIdSchema = sanitizedStringSchema(z.string().regex(STABLE_ID, 'must be a lowercase kebab-case stable ID'), 'stable ID');
65
- const sourceCommitSchema = sanitizedStringSchema(z.string().regex(SOURCE_COMMIT, 'must be a lowercase 40-character Git SHA'), 'source commit');
66
- const environmentScopeSchema = z.enum([
67
- 'hosted-production',
68
- 'production-reference',
69
- 'self-host-reference',
70
- 'local-development',
71
- 'example',
72
- 'unknown',
73
- ]);
74
- const fidelityCeilingSchema = z.enum([
75
- 'production-reference-only',
76
- 'self-host-reference-only',
77
- 'local-development-only',
78
- 'unknown',
79
- ]);
80
- const repositoryPathSchema = safeStringSchema('repository path').superRefine((value, context) => {
81
- if (value.includes('\\') ||
82
- value.startsWith('/') ||
83
- value.endsWith('/') ||
84
- normalize(value) !== value ||
85
- value.split('/').some((component) => component === '..' || component === '')) {
86
- context.addIssue({
87
- code: 'custom',
88
- message: 'must be a normalized repository-relative POSIX path',
89
- });
90
- }
91
- if (value.split('/').includes('.haystack')) {
92
- context.addIssue({
93
- code: 'custom',
94
- message: 'must not reference generated .haystack artifacts',
95
- });
96
- }
97
- });
98
- const evidenceSourceSchema = z
99
- .object({
100
- path: repositoryPathSchema,
101
- anchor: safeStringSchema('source anchor'),
102
- kind: stableIdSchema,
103
- locatorKind: z.enum([
104
- 'symbol',
105
- 'exact-snippet',
106
- 'line-span',
107
- 'manifest-key',
108
- 'config-key',
109
- ]),
110
- lines: z
111
- .object({
112
- start: z.number().int().positive(),
113
- end: z.number().int().positive(),
114
- })
115
- .strict()
116
- .optional(),
117
- })
118
- .strict()
119
- .superRefine((source, context) => {
120
- if (source.locatorKind === 'line-span') {
121
- if (!source.lines || source.lines.end < source.lines.start) {
122
- context.addIssue({
123
- code: 'custom',
124
- path: ['lines'],
125
- message: 'must contain positive start/end positions with end >= start for locatorKind line-span',
126
- });
127
- }
128
- }
129
- else if (source.lines !== undefined) {
130
- context.addIssue({
131
- code: 'custom',
132
- path: ['lines'],
133
- message: 'is only valid for locatorKind line-span',
134
- });
135
- }
136
- });
137
- const evidenceRecordSchema = z
138
- .object({
139
- id: stableIdSchema,
140
- subjectIds: z.array(stableIdSchema).min(1),
141
- assertion: safeStringSchema('evidence assertion'),
142
- environmentScope: environmentScopeSchema,
143
- authority: z.enum(['authoritative', 'indicative', 'non-production']),
144
- confidence: z.enum(['proven', 'strong', 'tentative']),
145
- claimKind: z.literal('topology-presence-census').optional(),
146
- sources: z.array(evidenceSourceSchema).min(1),
147
- })
148
- .strict();
149
- const budgetUsageSchema = z
150
- .object({
151
- repositoryInspectionCallsUsed: z.number().int().nonnegative(),
152
- elapsedSeconds: z.number().nonnegative(),
153
- })
154
- .strict();
155
- const provisionalUniverseScaffoldInputSchema = z
156
- .object({
157
- sourceCommit: sourceCommitSchema,
158
- application: z
159
- .object({
160
- id: stableIdSchema,
161
- requestedScope: stableIdSchema,
162
- evaluationMode: z.literal('real-production', {
163
- error: 'must be "real-production" because this scaffold records missing real-production authority',
164
- }),
165
- })
166
- .strict(),
167
- repositoryTopology: z
168
- .object({
169
- id: stableIdSchema,
170
- environmentScope: environmentScopeSchema,
171
- fidelityCeiling: fidelityCeilingSchema,
172
- evidenceIds: z.array(stableIdSchema).min(1),
173
- topologyPresenceCensus: z
174
- .object({
175
- status: z.literal('no-production-capable-topology'),
176
- subjectId: stableIdSchema,
177
- inspectedSurfaceKinds: z.tuple([
178
- z.literal(TOPOLOGY_PRESENCE_SURFACE_KINDS[0]),
179
- z.literal(TOPOLOGY_PRESENCE_SURFACE_KINDS[1]),
180
- z.literal(TOPOLOGY_PRESENCE_SURFACE_KINDS[2]),
181
- z.literal(TOPOLOGY_PRESENCE_SURFACE_KINDS[3]),
182
- z.literal(TOPOLOGY_PRESENCE_SURFACE_KINDS[4]),
183
- ]),
184
- productionCapableSurfaceCount: z.literal(0),
185
- evidenceIds: z.array(stableIdSchema).min(1),
186
- })
187
- .strict(),
188
- })
189
- .strict(),
190
- missingAuthority: z
191
- .object({
192
- id: stableIdSchema,
193
- kind: stableIdSchema,
194
- requiredFor: stableIdSchema,
195
- status: z.literal('unavailable-in-repository'),
196
- evidenceIds: z.array(stableIdSchema).min(1),
197
- })
198
- .strict(),
199
- accessRequest: z
200
- .object({
201
- id: stableIdSchema,
202
- source: stableIdSchema,
203
- reason: stableIdSchema,
204
- })
205
- .strict(),
206
- evidence: z.array(evidenceRecordSchema).min(1),
207
- coverage: z
208
- .object({
209
- scannedRoots: z.array(repositoryPathSchema).min(1),
210
- sourceKinds: z.array(stableIdSchema).min(1),
211
- })
212
- .strict(),
213
- budget: z
214
- .object({
215
- primaryDiscovery: budgetUsageSchema,
216
- authorityGate: budgetUsageSchema,
217
- overrunBlockerIds: z
218
- .object({
219
- primaryDiscovery: stableIdSchema.optional(),
220
- authorityGate: stableIdSchema.optional(),
221
- })
222
- .strict()
223
- .optional(),
224
- })
225
- .strict(),
226
- authorityBlocker: z
227
- .object({
228
- id: stableIdSchema,
229
- kind: z.enum([
230
- 'missing-authoritative-source',
231
- 'production-authority-access-required',
232
- ]),
233
- evidenceIds: z.array(stableIdSchema).min(1),
234
- })
235
- .strict(),
236
- })
237
- .strict();
238
- function assertUnique(values, label) {
239
- const seen = new Set();
240
- for (const value of values) {
241
- if (seen.has(value))
242
- throw new Error(`${label} contains duplicate stable ID "${value}"`);
243
- seen.add(value);
244
- }
245
- }
246
- function requireEvidenceSupport(references, evidenceById, subjectId, label) {
247
- for (const reference of references) {
248
- const evidence = evidenceById.get(reference);
249
- if (!evidence) {
250
- throw new Error(`${label} references unknown evidence ID "${reference}"`);
251
- }
252
- if (!evidence.subjectIds.includes(subjectId)) {
253
- throw new Error(`${label} evidence "${reference}" must include supported subject ID "${subjectId}"`);
254
- }
255
- }
256
- }
257
- function validateLinkedInput(input) {
258
- const evidenceIds = input.evidence.map((record) => record.id);
259
- assertUnique(evidenceIds, 'evidence');
260
- const evidenceById = new Map(input.evidence.map((record) => [record.id, record]));
261
- if (input.budget.primaryDiscovery.repositoryInspectionCallsUsed >
262
- PRIMARY_CALL_LIMIT) {
263
- throw new Error('budget.primaryDiscovery.repositoryInspectionCallsUsed must not exceed the scaffold\'s selected 48-call wave');
264
- }
265
- if (input.budget.authorityGate.repositoryInspectionCallsUsed >
266
- AUTHORITY_CALL_LIMIT) {
267
- throw new Error('budget.authorityGate.repositoryInspectionCallsUsed must not exceed the 6-call authority ceiling');
268
- }
269
- const primaryOverrun = input.budget.primaryDiscovery.elapsedSeconds > PRIMARY_ELAPSED_LIMIT_SECONDS;
270
- const authorityOverrun = input.budget.authorityGate.elapsedSeconds > AUTHORITY_ELAPSED_LIMIT_SECONDS;
271
- const configuredOverrunIds = input.budget.overrunBlockerIds;
272
- if (primaryOverrun !== (configuredOverrunIds?.primaryDiscovery !== undefined)) {
273
- throw new Error(primaryOverrun
274
- ? 'budget.overrunBlockerIds.primaryDiscovery is required for the exceeded budget.primary_discovery path'
275
- : 'budget.overrunBlockerIds.primaryDiscovery is unsupported when budget.primary_discovery is within limits');
276
- }
277
- if (authorityOverrun !== (configuredOverrunIds?.authorityGate !== undefined)) {
278
- throw new Error(authorityOverrun
279
- ? 'budget.overrunBlockerIds.authorityGate is required for the exceeded budget.primary_discovery.authority_gate path'
280
- : 'budget.overrunBlockerIds.authorityGate is unsupported when the authority gate is within limits');
281
- }
282
- if (input.budget.authorityGate.repositoryInspectionCallsUsed >
283
- input.budget.primaryDiscovery.repositoryInspectionCallsUsed ||
284
- input.budget.authorityGate.elapsedSeconds > input.budget.primaryDiscovery.elapsedSeconds) {
285
- throw new Error('authority-gate usage must not exceed total primary-discovery usage');
286
- }
287
- const universeIds = [
288
- input.application.id,
289
- input.repositoryTopology.id,
290
- input.missingAuthority.id,
291
- input.accessRequest.id,
292
- input.authorityBlocker.id,
293
- ...(configuredOverrunIds?.primaryDiscovery
294
- ? [configuredOverrunIds.primaryDiscovery]
295
- : []),
296
- ...(configuredOverrunIds?.authorityGate ? [configuredOverrunIds.authorityGate] : []),
297
- ...evidenceIds,
298
- ];
299
- assertUnique(universeIds, 'scaffold input');
300
- const evidenceSubjectIds = new Set([
301
- input.application.id,
302
- input.repositoryTopology.id,
303
- input.missingAuthority.id,
304
- ]);
305
- input.evidence.forEach((record) => {
306
- assertUnique(record.subjectIds, `evidence record "${record.id}" subjectIds`);
307
- record.subjectIds.forEach((subjectId) => {
308
- if (!evidenceSubjectIds.has(subjectId)) {
309
- throw new Error(`evidence record "${record.id}" references unknown universe subject ID "${subjectId}"`);
310
- }
311
- });
312
- });
313
- requireEvidenceSupport(input.repositoryTopology.evidenceIds, evidenceById, input.repositoryTopology.id, 'repositoryTopology.evidenceIds');
314
- const presenceCensus = input.repositoryTopology.topologyPresenceCensus;
315
- if (presenceCensus.subjectId !== input.repositoryTopology.id) {
316
- throw new Error('repositoryTopology.topologyPresenceCensus.subjectId must equal repositoryTopology.id');
317
- }
318
- requireEvidenceSupport(presenceCensus.evidenceIds, evidenceById, presenceCensus.subjectId, 'repositoryTopology.topologyPresenceCensus.evidenceIds');
319
- if (!presenceCensus.evidenceIds.some((evidenceId) => {
320
- const record = evidenceById.get(evidenceId);
321
- return (record?.claimKind === 'topology-presence-census' &&
322
- record.subjectIds.length === 1 &&
323
- record.subjectIds[0] === presenceCensus.subjectId);
324
- })) {
325
- throw new Error('repositoryTopology.topologyPresenceCensus.evidenceIds must include an exact topology-presence-census claim for repositoryTopology.id');
326
- }
327
- requireEvidenceSupport(input.missingAuthority.evidenceIds, evidenceById, input.missingAuthority.id, 'missingAuthority.evidenceIds');
328
- requireEvidenceSupport(input.authorityBlocker.evidenceIds, evidenceById, input.missingAuthority.id, 'authorityBlocker.evidenceIds');
329
- assertUnique(input.repositoryTopology.evidenceIds, 'repositoryTopology.evidenceIds');
330
- assertUnique(presenceCensus.evidenceIds, 'repositoryTopology.topologyPresenceCensus.evidenceIds');
331
- assertUnique(input.missingAuthority.evidenceIds, 'missingAuthority.evidenceIds');
332
- assertUnique(input.authorityBlocker.evidenceIds, 'authorityBlocker.evidenceIds');
333
- assertUnique(input.coverage.scannedRoots, 'coverage.scannedRoots');
334
- assertUnique(input.coverage.sourceKinds, 'coverage.sourceKinds');
335
- }
336
- function yamlText(value) {
337
- return stringifyYaml(value, { lineWidth: 0, aliasDuplicateObjects: false });
338
- }
339
- function renderArtifacts(input) {
340
- const budgetBlockers = [];
341
- const overrunIds = input.budget.overrunBlockerIds;
342
- if (overrunIds?.primaryDiscovery) {
343
- budgetBlockers.push({
344
- id: overrunIds.primaryDiscovery,
345
- kind: 'budget-exhausted',
346
- budget_path: 'budget.primary_discovery',
347
- });
348
- }
349
- if (overrunIds?.authorityGate) {
350
- budgetBlockers.push({
351
- id: overrunIds.authorityGate,
352
- kind: 'budget-exhausted',
353
- budget_path: 'budget.primary_discovery.authority_gate',
354
- });
355
- }
356
- const universe = {
357
- version: 1,
358
- map_revision: {
359
- source_commit: input.sourceCommit,
360
- previous_source_commit: null,
361
- mode: 'full-scan',
362
- changed_scope_ids: [],
363
- },
364
- application: {
365
- id: input.application.id,
366
- requested_scope: input.application.requestedScope,
367
- selected_production_scope: null,
368
- scope_status: 'provisional',
369
- evaluation_mode: input.application.evaluationMode,
370
- },
371
- production_basis: {
372
- repository_proven_topology: {
373
- environment_scope: input.repositoryTopology.environmentScope,
374
- fidelity_ceiling: input.repositoryTopology.fidelityCeiling,
375
- evidence_ids: input.repositoryTopology.evidenceIds,
376
- topology_presence_census: {
377
- status: input.repositoryTopology.topologyPresenceCensus.status,
378
- subject_id: input.repositoryTopology.topologyPresenceCensus.subjectId,
379
- inspected_surface_kinds: [
380
- ...input.repositoryTopology.topologyPresenceCensus
381
- .inspectedSurfaceKinds,
382
- ],
383
- production_capable_surface_count: input.repositoryTopology.topologyPresenceCensus
384
- .productionCapableSurfaceCount,
385
- evidence_ids: input.repositoryTopology.topologyPresenceCensus.evidenceIds,
386
- },
387
- },
388
- authoritative_sources: [],
389
- missing_authoritative_sources: [
390
- {
391
- id: input.missingAuthority.id,
392
- kind: input.missingAuthority.kind,
393
- required_for: input.missingAuthority.requiredFor,
394
- status: input.missingAuthority.status,
395
- evidence_ids: input.missingAuthority.evidenceIds,
396
- },
397
- ],
398
- access_request: {
399
- id: input.accessRequest.id,
400
- access: 'read-only',
401
- source: input.accessRequest.source,
402
- fields: [...PROVISIONAL_ACCESS_REQUEST_FIELDS],
403
- reason: input.accessRequest.reason,
404
- },
405
- alternate_topologies: [
406
- {
407
- id: input.repositoryTopology.id,
408
- environment_scope: input.repositoryTopology.environmentScope,
409
- selected: false,
410
- evidence_ids: input.repositoryTopology.evidenceIds,
411
- },
412
- ],
413
- },
414
- deployment_profiles: [],
415
- artifacts: [],
416
- services: [],
417
- interfaces: [],
418
- physical_bindings: [],
419
- resources: [],
420
- external_dependencies: [],
421
- consistency_groups: [],
422
- unresolved: [],
423
- };
424
- const evidence = {
425
- version: 1,
426
- coverage: {
427
- scanned_roots: input.coverage.scannedRoots,
428
- excluded_paths: [],
429
- source_kinds: input.coverage.sourceKinds,
430
- external_integration_scope: {
431
- method: 'not-entered-before-production-authority',
432
- status: 'partial',
433
- uncovered: ['active-hosted-integrations-and-secret-reference-names'],
434
- },
435
- inventory_closure: {
436
- runtime_roles: {
437
- status: 'partial',
438
- candidates: [],
439
- structured_registry_discovery: {
440
- status: 'partial',
441
- evidence_ids: input.repositoryTopology.evidenceIds,
442
- registries: [],
443
- uncovered: ['hosted-production-runtime-registries'],
444
- },
445
- uncovered: ['hosted-production-launch-surfaces'],
446
- },
447
- stateful_resources: {
448
- status: 'partial',
449
- candidates: [],
450
- state_surface_discovery: {
451
- status: 'partial',
452
- evidence_ids: input.repositoryTopology.evidenceIds,
453
- entries: [],
454
- uncovered: ['hosted-production-state-surfaces'],
455
- },
456
- logical_state_census: {
457
- status: 'partial',
458
- evidence_ids: input.repositoryTopology.evidenceIds,
459
- entries: [],
460
- uncovered: ['hosted-production-logical-state-identities'],
461
- },
462
- structured_registry_discovery: {
463
- status: 'partial',
464
- evidence_ids: input.repositoryTopology.evidenceIds,
465
- registries: [],
466
- uncovered: ['hosted-production-state-registries'],
467
- },
468
- uncovered: ['hosted-production-state'],
469
- },
470
- external_dependencies: {
471
- status: 'partial',
472
- candidates: [],
473
- structured_registry_discovery: {
474
- status: 'partial',
475
- evidence_ids: input.repositoryTopology.evidenceIds,
476
- registries: [],
477
- uncovered: ['hosted-production-integration-registries'],
478
- },
479
- uncovered: ['hosted-production-egress'],
480
- },
481
- },
482
- operational_surface_census: {
483
- status: 'partial',
484
- evidence_ids: input.repositoryTopology.evidenceIds,
485
- frontier_receipts: [
486
- {
487
- frontier: 'deployment-runtime-units',
488
- status: 'partial',
489
- surface_ids: [],
490
- evidence_ids: input.repositoryTopology.evidenceIds,
491
- uncovered: ['hosted-production-runtime-units'],
492
- },
493
- {
494
- frontier: 'state-materialization-identities',
495
- status: 'partial',
496
- surface_ids: [],
497
- evidence_ids: input.repositoryTopology.evidenceIds,
498
- uncovered: ['hosted-production-state-identities'],
499
- },
500
- {
501
- frontier: 'outbound-host-operation-contracts',
502
- status: 'partial',
503
- surface_ids: [],
504
- evidence_ids: input.repositoryTopology.evidenceIds,
505
- uncovered: ['hosted-production-outbound-host-operations'],
506
- },
507
- {
508
- frontier: 'required-inbound-pull-contracts',
509
- status: 'partial',
510
- surface_ids: [],
511
- evidence_ids: input.repositoryTopology.evidenceIds,
512
- uncovered: ['hosted-production-inbound-pull-operations'],
513
- },
514
- ],
515
- surfaces: [],
516
- uncovered: ['hosted-production-operational-surfaces'],
517
- },
518
- selector_surface_census: {
519
- status: 'partial',
520
- evidence_ids: input.repositoryTopology.evidenceIds,
521
- discovery_roots: [],
522
- surfaces: [],
523
- uncovered: ['hosted-production-selector-surfaces'],
524
- },
525
- },
526
- evidence: input.evidence.map((record) => ({
527
- id: record.id,
528
- subject_ids: record.subjectIds,
529
- assertion: record.assertion,
530
- ...(record.claimKind === undefined
531
- ? {}
532
- : { claim_kind: record.claimKind }),
533
- environment_scope: record.environmentScope,
534
- authority: record.authority,
535
- sources: record.sources.map((source) => ({
536
- path: source.path,
537
- ...(source.lines === undefined ? {} : { lines: source.lines }),
538
- anchor: source.anchor,
539
- kind: source.kind,
540
- locator_kind: source.locatorKind,
541
- })),
542
- confidence: record.confidence,
543
- })),
544
- };
545
- const review = {
546
- version: 1,
547
- inventory_closure_audit: {
548
- status: 'deferred',
549
- reason: 'inventory-closure-partial',
550
- },
551
- reviewer: {
552
- status: 'deferred',
553
- reason: 'production-authority-required',
554
- },
555
- budget: {
556
- primary_discovery: {
557
- call_limit: PRIMARY_CALL_LIMIT,
558
- elapsed_limit_seconds: PRIMARY_ELAPSED_LIMIT_SECONDS,
559
- repository_inspection_calls_used: input.budget.primaryDiscovery.repositoryInspectionCallsUsed,
560
- elapsed_seconds: input.budget.primaryDiscovery.elapsedSeconds,
561
- authority_gate: {
562
- call_limit: AUTHORITY_CALL_LIMIT,
563
- elapsed_limit_seconds: AUTHORITY_ELAPSED_LIMIT_SECONDS,
564
- repository_inspection_calls_used: input.budget.authorityGate.repositoryInspectionCallsUsed,
565
- elapsed_seconds: input.budget.authorityGate.elapsedSeconds,
566
- },
567
- },
568
- },
569
- termination: {
570
- reason: 'missing-production-authority',
571
- phase: 'authority-gate',
572
- },
573
- validation: {
574
- command: 'haystack skills validate-universe --json',
575
- validator_contract_revision: VALIDATOR_CONTRACT_REVISION,
576
- status: 'passed',
577
- },
578
- status: 'blocked',
579
- blocking_items: [
580
- {
581
- id: input.authorityBlocker.id,
582
- kind: input.authorityBlocker.kind,
583
- evidence_ids: input.authorityBlocker.evidenceIds,
584
- },
585
- ...budgetBlockers,
586
- ],
587
- };
588
- return {
589
- 'universe.yml': yamlText(universe),
590
- 'evidence.yml': yamlText(evidence),
591
- 'review.yml': yamlText(review),
592
- };
593
- }
594
- function assertSafeOutputDirectory(outputDirectory) {
595
- if (!isAbsolute(outputDirectory) || normalize(outputDirectory) !== outputDirectory) {
596
- throw new Error('output directory must be an absolute normalized path');
597
- }
598
- const resolved = resolve(outputDirectory);
599
- const parent = dirname(resolved);
600
- const repositoryRoot = dirname(parent);
601
- if (basename(resolved) !== 'cloud-verifier' ||
602
- basename(parent) !== '.haystack' ||
603
- repositoryRoot === parsePath(resolved).root) {
604
- throw new Error('output directory must be an absolute <repository>/.haystack/cloud-verifier path');
605
- }
606
- const root = parsePath(resolved).root;
607
- let current = root;
608
- for (const component of relative(root, resolved).split(sep)) {
609
- current = join(current, component);
610
- const metadata = lstatSync(current, { throwIfNoEntry: false });
611
- if (metadata === undefined)
612
- continue;
613
- if (metadata.isSymbolicLink()) {
614
- throw new Error(`output directory must not traverse symlink "${current}"`);
615
- }
616
- if (!metadata.isDirectory()) {
617
- throw new Error(`output directory ancestor is not a directory: "${current}"`);
618
- }
619
- }
620
- if (lstatSync(resolved, { throwIfNoEntry: false }) !== undefined) {
621
- throw new Error('output directory already exists; refusing to overwrite an artifact set');
622
- }
623
- return resolved;
624
- }
625
- function digest(text) {
626
- return `sha256:${createHash('sha256').update(text, 'utf8').digest('hex')}`;
627
- }
628
- /**
629
- * Validates a pure, data-only provisional receipt input, validates the rendered
630
- * artifacts against the universe contract, then publishes the three-file set by
631
- * one directory rename. Cooperative scaffold calls serialize through a lock,
632
- * and every destination observed before publication fails closed.
633
- */
634
- export function scaffoldProvisionalUniverse(outputDirectory, rawInput) {
635
- const input = provisionalUniverseScaffoldInputSchema.parse(rawInput);
636
- validateLinkedInput(input);
637
- const texts = renderArtifacts(input);
638
- const validation = validateUniverseArtifactTexts(texts);
639
- if (validation.status !== 'valid') {
640
- throw new Error(`generated provisional artifacts failed validation: ${validation.issues
641
- .map((issue) => `${issue.file}:${issue.path}: ${issue.message}`)
642
- .join('; ')}`);
643
- }
644
- const safeOutputDirectory = assertSafeOutputDirectory(outputDirectory);
645
- const parent = dirname(safeOutputDirectory);
646
- mkdirSync(parent, { recursive: true, mode: 0o700 });
647
- assertSafeOutputDirectory(safeOutputDirectory);
648
- const lockDirectory = join(parent, '.cloud-verifier-scaffold-lock');
649
- mkdirSync(lockDirectory, { mode: 0o700 });
650
- let stagingDirectory = null;
651
- try {
652
- if (lstatSync(safeOutputDirectory, { throwIfNoEntry: false }) !== undefined) {
653
- throw new Error('output directory appeared during scaffold; refusing to overwrite it');
654
- }
655
- stagingDirectory = mkdtempSync(join(parent, '.cloud-verifier-staging-'));
656
- for (const name of ARTIFACT_NAMES) {
657
- writeFileSync(join(stagingDirectory, name), texts[name], {
658
- encoding: 'utf8',
659
- flag: 'wx',
660
- mode: 0o600,
661
- });
662
- }
663
- if (lstatSync(safeOutputDirectory, { throwIfNoEntry: false }) !== undefined) {
664
- throw new Error('output directory appeared during scaffold; refusing to overwrite it');
665
- }
666
- renameSync(stagingDirectory, safeOutputDirectory);
667
- stagingDirectory = null;
668
- }
669
- finally {
670
- if (stagingDirectory !== null) {
671
- rmSync(stagingDirectory, { recursive: true, force: true });
672
- }
673
- rmSync(lockDirectory, { recursive: true, force: true });
674
- }
675
- return {
676
- outputDirectory: safeOutputDirectory,
677
- texts,
678
- artifacts: {
679
- 'universe.yml': {
680
- bytes: Buffer.byteLength(texts['universe.yml'], 'utf8'),
681
- digest: digest(texts['universe.yml']),
682
- },
683
- 'evidence.yml': {
684
- bytes: Buffer.byteLength(texts['evidence.yml'], 'utf8'),
685
- digest: digest(texts['evidence.yml']),
686
- },
687
- 'review.yml': {
688
- bytes: Buffer.byteLength(texts['review.yml'], 'utf8'),
689
- digest: digest(texts['review.yml']),
690
- },
691
- },
692
- validationSummary: {
693
- universeIds: validation.summary.universe_ids,
694
- evidenceIds: validation.summary.evidence_ids,
695
- reviewIds: validation.summary.review_ids,
696
- },
697
- };
698
- }
699
- function readStrictJsonInput(inputPath) {
700
- const metadata = lstatSync(inputPath, { throwIfNoEntry: false });
701
- if (metadata === undefined) {
702
- throw new Error(`provisional universe input file does not exist: "${inputPath}"`);
703
- }
704
- if (metadata.isSymbolicLink() || !metadata.isFile()) {
705
- throw new Error('provisional universe input must be a non-symlink regular file');
706
- }
707
- if (metadata.size > PROVISIONAL_UNIVERSE_INPUT_MAX_BYTES) {
708
- throw new Error(`provisional universe input exceeds ${PROVISIONAL_UNIVERSE_INPUT_MAX_BYTES} bytes`);
709
- }
710
- let descriptor = null;
711
- let bytes;
712
- try {
713
- descriptor = openSync(inputPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
714
- const openedMetadata = fstatSync(descriptor);
715
- if (!openedMetadata.isFile()) {
716
- throw new Error('provisional universe input must be a non-symlink regular file');
717
- }
718
- if (openedMetadata.size > PROVISIONAL_UNIVERSE_INPUT_MAX_BYTES) {
719
- throw new Error(`provisional universe input exceeds ${PROVISIONAL_UNIVERSE_INPUT_MAX_BYTES} bytes`);
720
- }
721
- bytes = readFileSync(descriptor);
722
- }
723
- finally {
724
- if (descriptor !== null)
725
- closeSync(descriptor);
726
- }
727
- if (bytes.byteLength > PROVISIONAL_UNIVERSE_INPUT_MAX_BYTES) {
728
- throw new Error(`provisional universe input exceeds ${PROVISIONAL_UNIVERSE_INPUT_MAX_BYTES} bytes`);
729
- }
730
- let text;
731
- try {
732
- text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
733
- }
734
- catch {
735
- throw new Error('provisional universe input must be valid UTF-8 JSON');
736
- }
737
- try {
738
- return JSON.parse(text);
739
- }
740
- catch (error) {
741
- throw new Error(`provisional universe input is malformed JSON: ${error.message}`);
742
- }
743
- }
744
- function currentCleanRepositoryCommit(root) {
745
- let head;
746
- try {
747
- head = execFileSync('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
748
- cwd: root,
749
- encoding: 'utf8',
750
- stdio: ['ignore', 'pipe', 'pipe'],
751
- }).trim();
752
- }
753
- catch {
754
- throw new Error('scaffold-provisional-universe requires a Git repository with a current commit');
755
- }
756
- const changes = execFileSync('git', [
757
- 'status',
758
- '--porcelain=v1',
759
- '--untracked-files=all',
760
- '--',
761
- '.',
762
- ':(exclude).haystack',
763
- ':(exclude).haystack/**',
764
- ], {
765
- cwd: root,
766
- encoding: 'utf8',
767
- stdio: ['ignore', 'pipe', 'pipe'],
768
- maxBuffer: 4 * 1024 * 1024,
769
- });
770
- if (changes.length > 0) {
771
- throw new Error('scaffold-provisional-universe requires a clean Git worktree outside .haystack');
772
- }
773
- return head;
774
- }
775
- /** CLI adapter for later command registration. Always emits one compact JSON line. */
776
- export function scaffoldProvisionalUniverseCommand(options) {
777
- if (typeof options.input !== 'string' || options.input.length === 0) {
778
- throw new Error('--input must name an absolute or current-directory-relative JSON file');
779
- }
780
- const root = findGitRoot();
781
- if (!root) {
782
- throw new Error('scaffold-provisional-universe must be run inside a Git repository');
783
- }
784
- const inputPath = isAbsolute(options.input)
785
- ? normalize(options.input)
786
- : resolve(process.cwd(), options.input);
787
- const rawInput = readStrictJsonInput(inputPath);
788
- const input = provisionalUniverseScaffoldInputSchema.parse(rawInput);
789
- const head = currentCleanRepositoryCommit(root);
790
- if (input.sourceCommit !== head) {
791
- throw new Error(`sourceCommit must equal the current Git HEAD "${head}"`);
792
- }
793
- const result = scaffoldProvisionalUniverse(join(root, '.haystack', 'cloud-verifier'), input);
794
- const metadata = {
795
- status: 'created',
796
- output_directory: result.outputDirectory,
797
- artifacts: result.artifacts,
798
- validation: {
799
- validator_contract_revision: VALIDATOR_CONTRACT_REVISION,
800
- universe_ids: result.validationSummary.universeIds,
801
- evidence_ids: result.validationSummary.evidenceIds,
802
- review_ids: result.validationSummary.reviewIds,
803
- },
804
- };
805
- console.log(JSON.stringify(metadata));
806
- }