@wrongstack/requirement-intake 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5,18 +5,18 @@
5
5
  * development requests as structured intake records. Upstream of spec-driven
6
6
  * development; this module never plans, specifies, or implements.
7
7
  */
8
- export { INTAKE_ID_PREFIX, REQUEST_TYPES, INTAKE_STATUSES, INTAKE_PRIORITIES, INTAKE_FIELD_SOURCES, INTAKE_FIELDS, INTAKE_ATTACHMENT_KINDS, RELATED_RESOURCE_KINDS, INTAKE_QUESTION_STATUSES, SUGGESTION_STATUSES, SUGGESTION_KINDS, MAX_REQUEST_LENGTH, MAX_TITLE_LENGTH, MAX_SUMMARY_LENGTH, MAX_STRING_FIELD_LENGTH, MAX_ARRAY_ITEMS, MAX_ATTACHMENTS, MAX_RELATED_RESOURCES, MAX_METADATA_ENTRIES, MAX_METADATA_BYTES, MAX_ANSWER_LENGTH, MAX_IDEMPOTENCY_KEY_LENGTH, MAX_REFERENCE_LENGTH, MAX_QUESTION_LENGTH, MAX_HISTORY_ENTRIES, MAX_SUGGESTIONS, DEFAULT_INTAKE_QUESTIONS, type RequestType, type IntakeStatus, type IntakePriority, type IntakeFieldSource, type IntakeField, type IntakeAttachmentKind, type RelatedResourceKind, type IntakeQuestionStatus, type SuggestionStatus, type SuggestionKind, type IntakeQuestionTemplate, } from './constants.js';
9
- export { INTAKE_EVENT_NAMES, type IntakeActor, type IntakeContext, type IntakeAttachment, type RelatedResource, type IntakeAnswer, type IntakeQuestion, type ChangeHistoryEntry, type LlmSuggestionProposal, type RequirementIntakeRecord, type CreateIntakeInput, type IntakeAttachmentInput, type RelatedResourceInput, type IntakeQuestionTemplateInput, type UpdateIntakeInput, type AddAnswerInput, type AttachResourceInput, type IntakeEvent, type IntakeEventName, } from './types.js';
10
- export { IntakeError, IntakeValidationError, IntakeNotFoundError, IntakeStateTransitionError, IntakeStatusLockedError, IntakeConflictError, IntakeAuthorizationError, IntakeSuggestionError, type IntakeErrorCode, type IntakeValidationIssue, } from './errors.js';
11
- export { requestTypeSchema, prioritySchema, metadataSchema, attachmentInputSchema, relatedResourceInputSchema, questionTemplateInputSchema, createIntakeSchema, updateIntakeSchema, answerInputSchema, attachResourceInputSchema, normalizeRequestType, isBlank, parseWithIssues, validateCreateInput, validateUpdateInput, validateAnswerInput, validateAttachResourceInput, validateAttachmentInput, validateRelatedResourceInput, validateQuestionTemplateInput, validateFieldSource, deterministicSummary, deterministicTitle, } from './validation.js';
12
- export { ALLOWED_TRANSITIONS, MUTABLE_STATUSES, canTransition, assertTransition, isMutableStatus, isTerminalStatus, isKnownStatus, } from './lifecycle.js';
13
- export { buildInitialQuestions, pendingQuestions, upsertQuestion } from './questions.js';
14
- export { INTAKE_OPERATIONS, AllowAllIntakeAuthorizer, DenyAllIntakeAuthorizer, ProjectMembershipIntakeAuthorizer, type IntakeAuthorizer, type IntakeOperation, type ProjectMembershipIntakeAuthorizerOptions, } from './authorization.js';
15
- export { NoopIntakeLogger, InMemoryIntakeLogger, type IntakeLogger, type IntakeLogFields, } from './logger.js';
16
- export { INTAKE_COUNTERS, INTAKE_TIMERS, NoopIntakeMetrics, InMemoryIntakeMetrics, type IntakeCounter, type IntakeTimer, type IntakeMetrics, } from './metrics.js';
8
+ export { AllowAllIntakeAuthorizer, DenyAllIntakeAuthorizer, INTAKE_OPERATIONS, type IntakeAuthorizer, type IntakeOperation, ProjectMembershipIntakeAuthorizer, type ProjectMembershipIntakeAuthorizerOptions, } from './authorization.js';
9
+ export { DEFAULT_INTAKE_QUESTIONS, INTAKE_ATTACHMENT_KINDS, INTAKE_FIELD_SOURCES, INTAKE_FIELDS, INTAKE_ID_PREFIX, INTAKE_PRIORITIES, INTAKE_QUESTION_STATUSES, INTAKE_STATUSES, type IntakeAttachmentKind, type IntakeField, type IntakeFieldSource, type IntakePriority, type IntakeQuestionStatus, type IntakeQuestionTemplate, type IntakeStatus, MAX_ANSWER_LENGTH, MAX_ARRAY_ITEMS, MAX_ATTACHMENTS, MAX_HISTORY_ENTRIES, MAX_IDEMPOTENCY_KEY_LENGTH, MAX_METADATA_BYTES, MAX_METADATA_ENTRIES, MAX_QUESTION_LENGTH, MAX_REFERENCE_LENGTH, MAX_RELATED_RESOURCES, MAX_REQUEST_LENGTH, MAX_STRING_FIELD_LENGTH, MAX_SUGGESTIONS, MAX_SUMMARY_LENGTH, MAX_TITLE_LENGTH, RELATED_RESOURCE_KINDS, REQUEST_TYPES, type RelatedResourceKind, type RequestType, SUGGESTION_KINDS, SUGGESTION_STATUSES, type SuggestionKind, type SuggestionStatus, } from './constants.js';
10
+ export { IntakeAuthorizationError, IntakeConflictError, IntakeError, type IntakeErrorCode, IntakeNotFoundError, IntakeStateTransitionError, IntakeStatusLockedError, IntakeSuggestionError, IntakeValidationError, type IntakeValidationIssue, } from './errors.js';
17
11
  export { IntakeEventEmitter } from './events.js';
18
- export { RequirementIntakeStore, newIntakeId, type RequirementIntakeStoreOptions, type IntakeIndexEntry, type StoreUpdateOptions, type StoreCreateResult, } from './store.js';
19
- export { llmSuggestionOutputSchema, validateLlmSuggestionOutput, toProposals, assertSuggestionString, type LlmSuggestionRequest, type LlmSuggestionOutput, type LlmSuggestionGenerator, type NormalizedLlmSuggestion, } from './suggestions.js';
20
- export { RequirementIntakeService, type RequirementIntakeServiceOptions, type IntakeCreateResult, type IntakeSubmitResult, type IntakeListFilter, } from './service.js';
21
- export { VIBE_TAG_REGEX, hasVibeTag, stripVibeTag, deriveVibeState, type VibeProtocolStage, type VibeProtocolState, } from './vibe.js';
12
+ export { ALLOWED_TRANSITIONS, assertTransition, canTransition, isKnownStatus, isMutableStatus, isTerminalStatus, MUTABLE_STATUSES, } from './lifecycle.js';
13
+ export { InMemoryIntakeLogger, type IntakeLogFields, type IntakeLogger, NoopIntakeLogger, } from './logger.js';
14
+ export { INTAKE_COUNTERS, INTAKE_TIMERS, InMemoryIntakeMetrics, type IntakeCounter, type IntakeMetrics, type IntakeTimer, NoopIntakeMetrics, } from './metrics.js';
15
+ export { buildInitialQuestions, pendingQuestions, upsertQuestion } from './questions.js';
16
+ export { type IntakeCreateResult, type IntakeListFilter, type IntakeSubmitResult, RequirementIntakeService, type RequirementIntakeServiceOptions, } from './service.js';
17
+ export { type IntakeIndexEntry, newIntakeId, RequirementIntakeStore, type RequirementIntakeStoreOptions, type StoreCreateResult, type StoreUpdateOptions, } from './store.js';
18
+ export { assertSuggestionString, type LlmSuggestionGenerator, type LlmSuggestionOutput, type LlmSuggestionRequest, llmSuggestionOutputSchema, type NormalizedLlmSuggestion, toProposals, validateLlmSuggestionOutput, } from './suggestions.js';
19
+ export { type AddAnswerInput, type AttachResourceInput, type ChangeHistoryEntry, type CreateIntakeInput, INTAKE_EVENT_NAMES, type IntakeActor, type IntakeAnswer, type IntakeAttachment, type IntakeAttachmentInput, type IntakeContext, type IntakeEvent, type IntakeEventName, type IntakeQuestion, type IntakeQuestionTemplateInput, type LlmSuggestionProposal, type RelatedResource, type RelatedResourceInput, type RequirementIntakeRecord, type UpdateIntakeInput, } from './types.js';
20
+ export { answerInputSchema, attachmentInputSchema, attachResourceInputSchema, createIntakeSchema, deterministicSummary, deterministicTitle, isBlank, metadataSchema, normalizeRequestType, parseWithIssues, prioritySchema, questionTemplateInputSchema, relatedResourceInputSchema, requestTypeSchema, updateIntakeSchema, validateAnswerInput, validateAttachmentInput, validateAttachResourceInput, validateCreateInput, validateFieldSource, validateQuestionTemplateInput, validateRelatedResourceInput, validateUpdateInput, } from './validation.js';
21
+ export { deriveVibeState, hasVibeTag, stripVibeTag, VIBE_TAG_REGEX, type VibeProtocolStage, type VibeProtocolState, } from './vibe.js';
22
22
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,3 +1,46 @@
1
+ // src/authorization.ts
2
+ var INTAKE_OPERATIONS = [
3
+ "create",
4
+ "read",
5
+ "list",
6
+ "update",
7
+ "answer",
8
+ "attach",
9
+ "suggest",
10
+ "accept_suggestion",
11
+ "reject_suggestion",
12
+ "submit",
13
+ "cancel",
14
+ "archive"
15
+ ];
16
+ var AllowAllIntakeAuthorizer = class {
17
+ isAllowed(_operation, _ctx, _record) {
18
+ return true;
19
+ }
20
+ };
21
+ var DenyAllIntakeAuthorizer = class {
22
+ isAllowed(_operation, _ctx, _record) {
23
+ return false;
24
+ }
25
+ };
26
+ var ProjectMembershipIntakeAuthorizer = class {
27
+ projectsOf;
28
+ ownerOnly;
29
+ constructor(options) {
30
+ this.projectsOf = options.projectsOf;
31
+ this.ownerOnly = options.ownerOnlyOperations ?? /* @__PURE__ */ new Set();
32
+ }
33
+ async isAllowed(operation, ctx, record) {
34
+ if (record && record.projectId !== ctx.projectId) return false;
35
+ const memberships = await this.projectsOf(ctx.id, ctx.type);
36
+ if (!memberships.has(ctx.projectId)) return false;
37
+ if (this.ownerOnly.has(operation) && record) {
38
+ return record.requestedBy === ctx.id;
39
+ }
40
+ return true;
41
+ }
42
+ };
43
+
1
44
  // src/constants.ts
2
45
  var INTAKE_ID_PREFIX = "reqi_";
3
46
  var ANSWER_ID_PREFIX = "ans_";
@@ -97,16 +140,6 @@ var DEFAULT_INTAKE_QUESTIONS = [
97
140
  }
98
141
  ];
99
142
 
100
- // src/types.ts
101
- var INTAKE_EVENT_NAMES = [
102
- "RequirementIntakeCreated",
103
- "RequirementIntakeUpdated",
104
- "RequirementIntakeInformationRequested",
105
- "RequirementIntakeSubmitted",
106
- "RequirementIntakeCancelled",
107
- "RequirementIntakeArchived"
108
- ];
109
-
110
143
  // src/errors.ts
111
144
  var IntakeError = class extends Error {
112
145
  code;
@@ -176,176 +209,39 @@ var IntakeSuggestionError = class extends IntakeError {
176
209
  }
177
210
  };
178
211
 
179
- // src/validation.ts
180
- import { z } from "zod";
181
- var requestTypeSchema = z.enum(REQUEST_TYPES);
182
- var prioritySchema = z.enum(INTAKE_PRIORITIES);
183
- var intakeQuestionStatusSchema = z.enum(INTAKE_QUESTION_STATUSES);
184
- var fieldSourceSchema = z.enum(["user", "llm", "deterministic", "system"]);
185
- var httpUrlSchema = z.string().max(MAX_REFERENCE_LENGTH).refine((value) => {
186
- try {
187
- const url = new URL(value);
188
- return url.protocol === "http:" || url.protocol === "https:";
189
- } catch {
190
- return false;
191
- }
192
- }, "must be a valid http(s) URL");
193
- var idSchema = z.string().min(1).max(128).refine((value) => value.trim().length > 0, "must not be blank");
194
- var nonBlankString = (max, label) => z.string().max(max, `${label} exceeds the maximum length of ${max} characters`).refine((value) => value.trim().length > 0, `${label} must not be empty or whitespace-only`);
195
- var attachmentInputSchema = z.object({
196
- name: nonBlankString(200, "attachment name"),
197
- kind: z.enum(INTAKE_ATTACHMENT_KINDS),
198
- path: z.string().max(MAX_REFERENCE_LENGTH).optional(),
199
- url: httpUrlSchema.optional(),
200
- sizeBytes: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
201
- mimeType: z.string().max(100).optional()
202
- }).refine((value) => value.path === void 0 !== (value.url === void 0), {
203
- message: "exactly one of path or url is required"
204
- });
205
- var relatedResourceInputSchema = z.object({
206
- kind: z.enum(RELATED_RESOURCE_KINDS),
207
- reference: nonBlankString(MAX_REFERENCE_LENGTH, "related resource reference"),
208
- title: z.string().max(200).optional()
209
- });
210
- var questionTemplateInputSchema = z.object({
211
- field: nonBlankString(64, "question field"),
212
- question: nonBlankString(MAX_QUESTION_LENGTH, "question text"),
213
- required: z.boolean().optional()
214
- });
215
- var metadataSchema = z.record(z.string().max(64), z.unknown()).superRefine((value, ctx) => {
216
- const keys = Object.keys(value);
217
- if (keys.length > MAX_METADATA_ENTRIES) {
218
- ctx.addIssue({
219
- code: "custom",
220
- path: [],
221
- message: `metadata exceeds the maximum of ${MAX_METADATA_ENTRIES} entries`
222
- });
212
+ // src/events.ts
213
+ var MAX_LISTENERS = 200;
214
+ var IntakeEventEmitter = class {
215
+ listeners = /* @__PURE__ */ new Set();
216
+ /** Publish an event. Listener errors are swallowed so one bad listener cannot break others. */
217
+ emit(event, data) {
218
+ const payload = { event, timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...data };
219
+ for (const listener of [...this.listeners]) {
220
+ try {
221
+ listener(payload);
222
+ } catch {
223
+ }
224
+ }
223
225
  }
224
- let bytes = 0;
225
- try {
226
- bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
227
- } catch {
228
- ctx.addIssue({ code: "custom", path: [], message: "metadata must be JSON-serializable" });
229
- return;
226
+ /** Subscribe; returns a disposer. Past the cap, returns a no-op disposer. */
227
+ subscribe(listener) {
228
+ if (this.listeners.size >= MAX_LISTENERS) {
229
+ const message = "Requirement intake event-emitter listener limit reached \u2014 callers must dispose subscriptions";
230
+ if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
231
+ process.emitWarning(message, "IntakeEventEmitterWarning");
232
+ }
233
+ return () => {
234
+ };
235
+ }
236
+ this.listeners.add(listener);
237
+ return () => {
238
+ this.listeners.delete(listener);
239
+ };
230
240
  }
231
- if (bytes > MAX_METADATA_BYTES) {
232
- ctx.addIssue({
233
- code: "custom",
234
- path: [],
235
- message: `metadata exceeds the maximum size of ${MAX_METADATA_BYTES} bytes`
236
- });
241
+ get listenerCount() {
242
+ return this.listeners.size;
237
243
  }
238
- });
239
- var optionalString = (max) => z.string().max(max).optional();
240
- var optionalStringArray = z.array(z.string().trim().min(1).max(MAX_STRING_FIELD_LENGTH)).max(MAX_ARRAY_ITEMS).optional();
241
- var optionalAttachmentArray = z.array(attachmentInputSchema).max(MAX_ATTACHMENTS).optional();
242
- var optionalRelatedResourceArray = z.array(relatedResourceInputSchema).max(MAX_RELATED_RESOURCES).optional();
243
- var createIntakeSchema = z.object({
244
- projectId: idSchema,
245
- originalRequest: nonBlankString(MAX_REQUEST_LENGTH, "originalRequest"),
246
- title: optionalString(MAX_TITLE_LENGTH),
247
- requestType: z.string().max(64).optional(),
248
- priority: prioritySchema.optional(),
249
- requestedBy: idSchema,
250
- businessGoal: optionalString(MAX_STRING_FIELD_LENGTH),
251
- targetUsers: optionalStringArray,
252
- expectedOutcome: optionalString(MAX_STRING_FIELD_LENGTH),
253
- scopeNotes: optionalString(MAX_STRING_FIELD_LENGTH),
254
- constraints: optionalStringArray,
255
- providedContext: optionalStringArray,
256
- attachments: optionalAttachmentArray,
257
- relatedResources: optionalRelatedResourceArray,
258
- metadata: metadataSchema.optional(),
259
- idempotencyKey: z.string().trim().max(MAX_IDEMPOTENCY_KEY_LENGTH).optional(),
260
- knownFields: z.array(z.string().trim().min(1).max(64)).max(MAX_ARRAY_ITEMS).optional(),
261
- questions: z.array(questionTemplateInputSchema).max(50).optional()
262
- }).strict();
263
- var updateIntakeSchema = z.object({
264
- title: optionalString(MAX_TITLE_LENGTH),
265
- requestType: z.string().max(64).optional(),
266
- priority: prioritySchema.optional(),
267
- businessGoal: optionalString(MAX_STRING_FIELD_LENGTH),
268
- targetUsers: optionalStringArray,
269
- expectedOutcome: optionalString(MAX_STRING_FIELD_LENGTH),
270
- scopeNotes: optionalString(MAX_STRING_FIELD_LENGTH),
271
- constraints: optionalStringArray,
272
- providedContext: optionalStringArray,
273
- metadata: metadataSchema.optional()
274
- }).strict();
275
- var answerInputSchema = z.object({
276
- field: nonBlankString(64, "answer field"),
277
- answer: nonBlankString(MAX_ANSWER_LENGTH, "answer").max(
278
- MAX_ANSWER_LENGTH,
279
- `answer exceeds the maximum length of ${MAX_ANSWER_LENGTH} characters`
280
- ),
281
- question: z.string().max(MAX_QUESTION_LENGTH).optional()
282
- });
283
- var attachResourceInputSchema = z.object({
284
- attachment: attachmentInputSchema.optional(),
285
- relatedResource: relatedResourceInputSchema.optional()
286
- }).refine((value) => value.attachment === void 0 !== (value.relatedResource === void 0), {
287
- message: "exactly one of attachment or relatedResource must be provided"
288
- });
289
- function normalizeRequestType(value) {
290
- if (typeof value !== "string") return "unspecified";
291
- const trimmed = value.trim().toLowerCase();
292
- if (trimmed.length === 0) return "unspecified";
293
- const normalized = trimmed.replace(/-/g, "_");
294
- if (REQUEST_TYPES.includes(normalized)) return normalized;
295
- return "other";
296
- }
297
- function isBlank(value) {
298
- return value.trim().length === 0;
299
- }
300
- function parseWithIssues(schema, data, scope) {
301
- const result = schema.safeParse(data);
302
- if (result.success) return result.data;
303
- const issues = result.error.issues.map((issue) => {
304
- const path2 = issue.path.join(".");
305
- return {
306
- field: path2 === "" ? scope : `${scope}.${path2}`,
307
- message: issue.message
308
- };
309
- });
310
- throw new IntakeValidationError(issues);
311
- }
312
- function validateCreateInput(data) {
313
- return parseWithIssues(createIntakeSchema, data, "createIntake");
314
- }
315
- function validateUpdateInput(data) {
316
- return parseWithIssues(updateIntakeSchema, data, "updateIntake");
317
- }
318
- function validateAnswerInput(data) {
319
- return parseWithIssues(answerInputSchema, data, "addAnswer");
320
- }
321
- function validateAttachResourceInput(data) {
322
- return parseWithIssues(attachResourceInputSchema, data, "attachResource");
323
- }
324
- function validateAttachmentInput(data) {
325
- return parseWithIssues(attachmentInputSchema, data, "attachment");
326
- }
327
- function validateRelatedResourceInput(data) {
328
- return parseWithIssues(relatedResourceInputSchema, data, "relatedResource");
329
- }
330
- function validateQuestionTemplateInput(data) {
331
- return parseWithIssues(questionTemplateInputSchema, data, "question");
332
- }
333
- function validateFieldSource(value) {
334
- const parsed = fieldSourceSchema.safeParse(value);
335
- if (parsed.success) return parsed.data;
336
- throw new IntakeValidationError([{ field: "source", message: "invalid field source" }]);
337
- }
338
- function deterministicSummary(originalRequest, maxLength = 240) {
339
- const collapsed = originalRequest.replace(/\s+/g, " ").trim();
340
- if (collapsed.length <= maxLength) return collapsed;
341
- return `${collapsed.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
342
- }
343
- function deterministicTitle(originalRequest, maxLength = MAX_TITLE_LENGTH) {
344
- const firstLine = originalRequest.split(/\r?\n/, 1)[0]?.trim() ?? "";
345
- if (firstLine.length === 0) return "Untitled request";
346
- if (firstLine.length <= maxLength) return firstLine;
347
- return `${firstLine.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
348
- }
244
+ };
349
245
 
350
246
  // src/lifecycle.ts
351
247
  var ALLOWED_TRANSITIONS = {
@@ -374,93 +270,6 @@ function isKnownStatus(value) {
374
270
  return INTAKE_STATUSES.includes(value);
375
271
  }
376
272
 
377
- // src/questions.ts
378
- import { ulid } from "@wrongstack/core/utils";
379
- var FIELD_TO_INPUT_PROPERTY = {
380
- description_scope: () => true,
381
- // always answered by the original request
382
- business_goal: (input) => input.businessGoal !== void 0 && input.businessGoal.trim().length > 0,
383
- target_users: (input) => (input.targetUsers?.length ?? 0) > 0,
384
- expected_outcome: (input) => input.expectedOutcome !== void 0 && input.expectedOutcome.trim().length > 0,
385
- project_component: (input) => (input.providedContext?.length ?? 0) > 0,
386
- constraints: (input) => (input.constraints?.length ?? 0) > 0,
387
- related_resources: (input) => (input.relatedResources?.length ?? 0) > 0,
388
- priority: (input) => input.priority !== void 0 && input.priority !== "unspecified",
389
- attachments: (input) => (input.attachments?.length ?? 0) > 0
390
- };
391
- function makeQuestion(template, order, skipped) {
392
- return {
393
- id: `${QUESTION_ID_PREFIX}${ulid()}`,
394
- field: template.field,
395
- question: template.question,
396
- required: template.required,
397
- status: skipped ? "skipped" : "unanswered",
398
- order
399
- };
400
- }
401
- function buildInitialQuestions(input, catalog = DEFAULT_INTAKE_QUESTIONS) {
402
- const known = new Set((input.knownFields ?? []).map((field) => field.trim()));
403
- return catalog.map((template, index) => {
404
- const answeredByInput = FIELD_TO_INPUT_PROPERTY[template.field]?.(input) ?? false;
405
- const skipped = answeredByInput || known.has(template.field);
406
- return makeQuestion(template, index, skipped);
407
- });
408
- }
409
- function pendingQuestions(record) {
410
- return record.questions.filter((question) => question.status === "unanswered").sort((a, b) => a.order - b.order);
411
- }
412
- function upsertQuestion(record, template) {
413
- const existing = record.questions.find(
414
- (question) => question.field === template.field && question.status !== "skipped"
415
- );
416
- if (existing) return false;
417
- record.questions.push(makeQuestion(template, record.questions.length, false));
418
- return true;
419
- }
420
-
421
- // src/authorization.ts
422
- var INTAKE_OPERATIONS = [
423
- "create",
424
- "read",
425
- "list",
426
- "update",
427
- "answer",
428
- "attach",
429
- "suggest",
430
- "accept_suggestion",
431
- "reject_suggestion",
432
- "submit",
433
- "cancel",
434
- "archive"
435
- ];
436
- var AllowAllIntakeAuthorizer = class {
437
- isAllowed(_operation, _ctx, _record) {
438
- return true;
439
- }
440
- };
441
- var DenyAllIntakeAuthorizer = class {
442
- isAllowed(_operation, _ctx, _record) {
443
- return false;
444
- }
445
- };
446
- var ProjectMembershipIntakeAuthorizer = class {
447
- projectsOf;
448
- ownerOnly;
449
- constructor(options) {
450
- this.projectsOf = options.projectsOf;
451
- this.ownerOnly = options.ownerOnlyOperations ?? /* @__PURE__ */ new Set();
452
- }
453
- async isAllowed(operation, ctx, record) {
454
- if (record && record.projectId !== ctx.projectId) return false;
455
- const memberships = await this.projectsOf(ctx.id, ctx.type);
456
- if (!memberships.has(ctx.projectId)) return false;
457
- if (this.ownerOnly.has(operation) && record) {
458
- return record.requestedBy === ctx.id;
459
- }
460
- return true;
461
- }
462
- };
463
-
464
273
  // src/logger.ts
465
274
  var NoopIntakeLogger = class {
466
275
  info(_scope, _message, _fields) {
@@ -530,51 +339,64 @@ var InMemoryIntakeMetrics = class {
530
339
  bucket.push(milliseconds);
531
340
  this.durations.set(timer, bucket);
532
341
  }
533
- count(counter) {
534
- return this.counters.get(counter) ?? 0;
535
- }
536
- /** Sum of recorded durations for a timer, or undefined when none recorded. */
537
- durationSum(timer) {
538
- const bucket = this.durations.get(timer);
539
- if (!bucket || bucket.length === 0) return void 0;
540
- return bucket.reduce((total, value) => total + value, 0);
541
- }
542
- };
543
-
544
- // src/events.ts
545
- var MAX_LISTENERS = 200;
546
- var IntakeEventEmitter = class {
547
- listeners = /* @__PURE__ */ new Set();
548
- /** Publish an event. Listener errors are swallowed so one bad listener cannot break others. */
549
- emit(event, data) {
550
- const payload = { event, timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...data };
551
- for (const listener of [...this.listeners]) {
552
- try {
553
- listener(payload);
554
- } catch {
555
- }
556
- }
557
- }
558
- /** Subscribe; returns a disposer. Past the cap, returns a no-op disposer. */
559
- subscribe(listener) {
560
- if (this.listeners.size >= MAX_LISTENERS) {
561
- const message = "Requirement intake event-emitter listener limit reached \u2014 callers must dispose subscriptions";
562
- if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
563
- process.emitWarning(message, "IntakeEventEmitterWarning");
564
- }
565
- return () => {
566
- };
567
- }
568
- this.listeners.add(listener);
569
- return () => {
570
- this.listeners.delete(listener);
571
- };
342
+ count(counter) {
343
+ return this.counters.get(counter) ?? 0;
572
344
  }
573
- get listenerCount() {
574
- return this.listeners.size;
345
+ /** Sum of recorded durations for a timer, or undefined when none recorded. */
346
+ durationSum(timer) {
347
+ const bucket = this.durations.get(timer);
348
+ if (!bucket || bucket.length === 0) return void 0;
349
+ return bucket.reduce((total, value) => total + value, 0);
575
350
  }
576
351
  };
577
352
 
353
+ // src/questions.ts
354
+ import { ulid } from "@wrongstack/core/utils";
355
+ var FIELD_TO_INPUT_PROPERTY = {
356
+ description_scope: () => true,
357
+ // always answered by the original request
358
+ business_goal: (input) => input.businessGoal !== void 0 && input.businessGoal.trim().length > 0,
359
+ target_users: (input) => (input.targetUsers?.length ?? 0) > 0,
360
+ expected_outcome: (input) => input.expectedOutcome !== void 0 && input.expectedOutcome.trim().length > 0,
361
+ project_component: (input) => (input.providedContext?.length ?? 0) > 0,
362
+ constraints: (input) => (input.constraints?.length ?? 0) > 0,
363
+ related_resources: (input) => (input.relatedResources?.length ?? 0) > 0,
364
+ priority: (input) => input.priority !== void 0 && input.priority !== "unspecified",
365
+ attachments: (input) => (input.attachments?.length ?? 0) > 0
366
+ };
367
+ function makeQuestion(template, order, skipped) {
368
+ return {
369
+ id: `${QUESTION_ID_PREFIX}${ulid()}`,
370
+ field: template.field,
371
+ question: template.question,
372
+ required: template.required,
373
+ status: skipped ? "skipped" : "unanswered",
374
+ order
375
+ };
376
+ }
377
+ function buildInitialQuestions(input, catalog = DEFAULT_INTAKE_QUESTIONS) {
378
+ const known = new Set((input.knownFields ?? []).map((field) => field.trim()));
379
+ return catalog.map((template, index) => {
380
+ const answeredByInput = FIELD_TO_INPUT_PROPERTY[template.field]?.(input) ?? false;
381
+ const skipped = answeredByInput || known.has(template.field);
382
+ return makeQuestion(template, index, skipped);
383
+ });
384
+ }
385
+ function pendingQuestions(record) {
386
+ return record.questions.filter((question) => question.status === "unanswered").sort((a, b) => a.order - b.order);
387
+ }
388
+ function upsertQuestion(record, template) {
389
+ const existing = record.questions.find(
390
+ (question) => question.field === template.field && question.status !== "skipped"
391
+ );
392
+ if (existing) return false;
393
+ record.questions.push(makeQuestion(template, record.questions.length, false));
394
+ return true;
395
+ }
396
+
397
+ // src/service-helpers.ts
398
+ import { ulid as ulid4 } from "@wrongstack/core/utils";
399
+
578
400
  // src/store.ts
579
401
  import { createHash } from "node:crypto";
580
402
  import * as fsp from "node:fs/promises";
@@ -794,8 +616,186 @@ function newIntakeId() {
794
616
  }
795
617
 
796
618
  // src/suggestions.ts
797
- import { z as z2 } from "zod";
798
619
  import { ulid as ulid3 } from "@wrongstack/core/utils";
620
+ import { z as z2 } from "zod";
621
+
622
+ // src/validation.ts
623
+ import { z } from "zod";
624
+ var requestTypeSchema = z.enum(REQUEST_TYPES);
625
+ var prioritySchema = z.enum(INTAKE_PRIORITIES);
626
+ var intakeQuestionStatusSchema = z.enum(INTAKE_QUESTION_STATUSES);
627
+ var fieldSourceSchema = z.enum(["user", "llm", "deterministic", "system"]);
628
+ var httpUrlSchema = z.string().max(MAX_REFERENCE_LENGTH).refine((value) => {
629
+ try {
630
+ const url = new URL(value);
631
+ return url.protocol === "http:" || url.protocol === "https:";
632
+ } catch {
633
+ return false;
634
+ }
635
+ }, "must be a valid http(s) URL");
636
+ var idSchema = z.string().min(1).max(128).refine((value) => value.trim().length > 0, "must not be blank");
637
+ var nonBlankString = (max, label) => z.string().max(max, `${label} exceeds the maximum length of ${max} characters`).refine((value) => value.trim().length > 0, `${label} must not be empty or whitespace-only`);
638
+ var attachmentInputSchema = z.object({
639
+ name: nonBlankString(200, "attachment name"),
640
+ kind: z.enum(INTAKE_ATTACHMENT_KINDS),
641
+ path: z.string().max(MAX_REFERENCE_LENGTH).optional(),
642
+ url: httpUrlSchema.optional(),
643
+ sizeBytes: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
644
+ mimeType: z.string().max(100).optional()
645
+ }).refine((value) => value.path === void 0 !== (value.url === void 0), {
646
+ message: "exactly one of path or url is required"
647
+ });
648
+ var relatedResourceInputSchema = z.object({
649
+ kind: z.enum(RELATED_RESOURCE_KINDS),
650
+ reference: nonBlankString(MAX_REFERENCE_LENGTH, "related resource reference"),
651
+ title: z.string().max(200).optional()
652
+ });
653
+ var questionTemplateInputSchema = z.object({
654
+ field: nonBlankString(64, "question field"),
655
+ question: nonBlankString(MAX_QUESTION_LENGTH, "question text"),
656
+ required: z.boolean().optional()
657
+ });
658
+ var metadataSchema = z.record(z.string().max(64), z.unknown()).superRefine((value, ctx) => {
659
+ const keys = Object.keys(value);
660
+ if (keys.length > MAX_METADATA_ENTRIES) {
661
+ ctx.addIssue({
662
+ code: "custom",
663
+ path: [],
664
+ message: `metadata exceeds the maximum of ${MAX_METADATA_ENTRIES} entries`
665
+ });
666
+ }
667
+ let bytes = 0;
668
+ try {
669
+ bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
670
+ } catch {
671
+ ctx.addIssue({ code: "custom", path: [], message: "metadata must be JSON-serializable" });
672
+ return;
673
+ }
674
+ if (bytes > MAX_METADATA_BYTES) {
675
+ ctx.addIssue({
676
+ code: "custom",
677
+ path: [],
678
+ message: `metadata exceeds the maximum size of ${MAX_METADATA_BYTES} bytes`
679
+ });
680
+ }
681
+ });
682
+ var optionalString = (max) => z.string().max(max).optional();
683
+ var optionalStringArray = z.array(z.string().trim().min(1).max(MAX_STRING_FIELD_LENGTH)).max(MAX_ARRAY_ITEMS).optional();
684
+ var optionalAttachmentArray = z.array(attachmentInputSchema).max(MAX_ATTACHMENTS).optional();
685
+ var optionalRelatedResourceArray = z.array(relatedResourceInputSchema).max(MAX_RELATED_RESOURCES).optional();
686
+ var createIntakeSchema = z.object({
687
+ projectId: idSchema,
688
+ originalRequest: nonBlankString(MAX_REQUEST_LENGTH, "originalRequest"),
689
+ title: optionalString(MAX_TITLE_LENGTH),
690
+ requestType: z.string().max(64).optional(),
691
+ priority: prioritySchema.optional(),
692
+ requestedBy: idSchema,
693
+ businessGoal: optionalString(MAX_STRING_FIELD_LENGTH),
694
+ targetUsers: optionalStringArray,
695
+ expectedOutcome: optionalString(MAX_STRING_FIELD_LENGTH),
696
+ scopeNotes: optionalString(MAX_STRING_FIELD_LENGTH),
697
+ constraints: optionalStringArray,
698
+ providedContext: optionalStringArray,
699
+ attachments: optionalAttachmentArray,
700
+ relatedResources: optionalRelatedResourceArray,
701
+ metadata: metadataSchema.optional(),
702
+ idempotencyKey: z.string().trim().max(MAX_IDEMPOTENCY_KEY_LENGTH).optional(),
703
+ knownFields: z.array(z.string().trim().min(1).max(64)).max(MAX_ARRAY_ITEMS).optional(),
704
+ questions: z.array(questionTemplateInputSchema).max(50).optional(),
705
+ isVibeMode: z.boolean().optional(),
706
+ vibeProtocol: z.any().optional()
707
+ }).strict();
708
+ var updateIntakeSchema = z.object({
709
+ title: optionalString(MAX_TITLE_LENGTH),
710
+ requestType: z.string().max(64).optional(),
711
+ priority: prioritySchema.optional(),
712
+ businessGoal: optionalString(MAX_STRING_FIELD_LENGTH),
713
+ targetUsers: optionalStringArray,
714
+ expectedOutcome: optionalString(MAX_STRING_FIELD_LENGTH),
715
+ scopeNotes: optionalString(MAX_STRING_FIELD_LENGTH),
716
+ constraints: optionalStringArray,
717
+ providedContext: optionalStringArray,
718
+ metadata: metadataSchema.optional(),
719
+ isVibeMode: z.boolean().optional(),
720
+ vibeProtocol: z.any().optional()
721
+ }).strict();
722
+ var answerInputSchema = z.object({
723
+ field: nonBlankString(64, "answer field"),
724
+ answer: nonBlankString(MAX_ANSWER_LENGTH, "answer").max(
725
+ MAX_ANSWER_LENGTH,
726
+ `answer exceeds the maximum length of ${MAX_ANSWER_LENGTH} characters`
727
+ ),
728
+ question: z.string().max(MAX_QUESTION_LENGTH).optional()
729
+ });
730
+ var attachResourceInputSchema = z.object({
731
+ attachment: attachmentInputSchema.optional(),
732
+ relatedResource: relatedResourceInputSchema.optional()
733
+ }).refine((value) => value.attachment === void 0 !== (value.relatedResource === void 0), {
734
+ message: "exactly one of attachment or relatedResource must be provided"
735
+ });
736
+ function normalizeRequestType(value) {
737
+ if (typeof value !== "string") return "unspecified";
738
+ const trimmed = value.trim().toLowerCase();
739
+ if (trimmed.length === 0) return "unspecified";
740
+ const normalized = trimmed.replace(/-/g, "_");
741
+ if (REQUEST_TYPES.includes(normalized)) return normalized;
742
+ return "other";
743
+ }
744
+ function isBlank(value) {
745
+ return value.trim().length === 0;
746
+ }
747
+ function parseWithIssues(schema, data, scope) {
748
+ const result = schema.safeParse(data);
749
+ if (result.success) return result.data;
750
+ const issues = result.error.issues.map((issue) => {
751
+ const path2 = issue.path.join(".");
752
+ return {
753
+ field: path2 === "" ? scope : `${scope}.${path2}`,
754
+ message: issue.message
755
+ };
756
+ });
757
+ throw new IntakeValidationError(issues);
758
+ }
759
+ function validateCreateInput(data) {
760
+ return parseWithIssues(createIntakeSchema, data, "createIntake");
761
+ }
762
+ function validateUpdateInput(data) {
763
+ return parseWithIssues(updateIntakeSchema, data, "updateIntake");
764
+ }
765
+ function validateAnswerInput(data) {
766
+ return parseWithIssues(answerInputSchema, data, "addAnswer");
767
+ }
768
+ function validateAttachResourceInput(data) {
769
+ return parseWithIssues(attachResourceInputSchema, data, "attachResource");
770
+ }
771
+ function validateAttachmentInput(data) {
772
+ return parseWithIssues(attachmentInputSchema, data, "attachment");
773
+ }
774
+ function validateRelatedResourceInput(data) {
775
+ return parseWithIssues(relatedResourceInputSchema, data, "relatedResource");
776
+ }
777
+ function validateQuestionTemplateInput(data) {
778
+ return parseWithIssues(questionTemplateInputSchema, data, "question");
779
+ }
780
+ function validateFieldSource(value) {
781
+ const parsed = fieldSourceSchema.safeParse(value);
782
+ if (parsed.success) return parsed.data;
783
+ throw new IntakeValidationError([{ field: "source", message: "invalid field source" }]);
784
+ }
785
+ function deterministicSummary(originalRequest, maxLength = 240) {
786
+ const collapsed = originalRequest.replace(/\s+/g, " ").trim();
787
+ if (collapsed.length <= maxLength) return collapsed;
788
+ return `${collapsed.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
789
+ }
790
+ function deterministicTitle(originalRequest, maxLength = MAX_TITLE_LENGTH) {
791
+ const newlineIdx = originalRequest.search(/\r?\n/);
792
+ const firstLine = (newlineIdx >= 0 ? originalRequest.slice(0, newlineIdx) : originalRequest).trim();
793
+ if (firstLine.length === 0) return "Untitled request";
794
+ if (firstLine.length <= maxLength) return firstLine;
795
+ return `${firstLine.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
796
+ }
797
+
798
+ // src/suggestions.ts
799
799
  var outputQuestionSchema = z2.object({
800
800
  field: z2.string().trim().min(1).max(64),
801
801
  question: z2.string().trim().min(1).max(MAX_QUESTION_LENGTH),
@@ -879,9 +879,6 @@ function assertSuggestionString(value, label, max) {
879
879
  return value;
880
880
  }
881
881
 
882
- // src/service-helpers.ts
883
- import { ulid as ulid4 } from "@wrongstack/core/utils";
884
-
885
882
  // src/vibe.ts
886
883
  var VIBE_TAG_REGEX = /\[VIBE\]/i;
887
884
  function hasVibeTag(text) {
@@ -1786,6 +1783,16 @@ var RequirementIntakeService = class {
1786
1783
  }
1787
1784
  }
1788
1785
  };
1786
+
1787
+ // src/types.ts
1788
+ var INTAKE_EVENT_NAMES = [
1789
+ "RequirementIntakeCreated",
1790
+ "RequirementIntakeUpdated",
1791
+ "RequirementIntakeInformationRequested",
1792
+ "RequirementIntakeSubmitted",
1793
+ "RequirementIntakeCancelled",
1794
+ "RequirementIntakeArchived"
1795
+ ];
1789
1796
  export {
1790
1797
  ALLOWED_TRANSITIONS,
1791
1798
  AllowAllIntakeAuthorizer,
package/dist/service.d.ts CHANGED
@@ -13,13 +13,13 @@
13
13
  * The original request is immutable after creation: no API path can change
14
14
  * `originalRequest`, and LLM suggestions can never write to it.
15
15
  */
16
- import { type IntakeQuestionTemplate, type IntakeStatus } from './constants.js';
17
16
  import type { IntakeAuthorizer } from './authorization.js';
17
+ import { type IntakeQuestionTemplate, type IntakeStatus } from './constants.js';
18
18
  import { IntakeEventEmitter } from './events.js';
19
19
  import { type IntakeLogger } from './logger.js';
20
20
  import { type IntakeMetrics } from './metrics.js';
21
- import { type LlmSuggestionGenerator } from './suggestions.js';
22
21
  import type { RequirementIntakeStore } from './store.js';
22
+ import { type LlmSuggestionGenerator } from './suggestions.js';
23
23
  import type { AddAnswerInput, AttachResourceInput, CreateIntakeInput, IntakeContext, IntakeEvent, IntakeQuestion, LlmSuggestionProposal, RequirementIntakeRecord, UpdateIntakeInput } from './types.js';
24
24
  export interface RequirementIntakeServiceOptions {
25
25
  store: RequirementIntakeStore;
@@ -128,6 +128,8 @@ export declare const createIntakeSchema: z.ZodObject<{
128
128
  question: z.ZodString;
129
129
  required: z.ZodOptional<z.ZodBoolean>;
130
130
  }, z.core.$strip>>>;
131
+ isVibeMode: z.ZodOptional<z.ZodBoolean>;
132
+ vibeProtocol: z.ZodOptional<z.ZodAny>;
131
133
  }, z.core.$strict>;
132
134
  export declare const updateIntakeSchema: z.ZodObject<{
133
135
  title: z.ZodOptional<z.ZodString>;
@@ -146,6 +148,8 @@ export declare const updateIntakeSchema: z.ZodObject<{
146
148
  constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
147
149
  providedContext: z.ZodOptional<z.ZodArray<z.ZodString>>;
148
150
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
151
+ isVibeMode: z.ZodOptional<z.ZodBoolean>;
152
+ vibeProtocol: z.ZodOptional<z.ZodAny>;
149
153
  }, z.core.$strict>;
150
154
  export declare const answerInputSchema: z.ZodObject<{
151
155
  field: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/requirement-intake",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Requirements Intake — collect, preserve, validate, normalize, and submit unstructured software development requests as structured intake records. Upstream of spec-driven development: it never plans, specifies, or implements.",
6
6
  "repository": {
@@ -27,7 +27,7 @@
27
27
  "!dist/**/*.map"
28
28
  ],
29
29
  "dependencies": {
30
- "@wrongstack/core": "1.0.2",
30
+ "@wrongstack/core": "1.0.3",
31
31
  "zod": "4.4.3"
32
32
  },
33
33
  "devDependencies": {