@wrongstack/requirement-intake 0.306.4 → 0.307.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -18,4 +18,5 @@ export { IntakeEventEmitter } from './events.js';
18
18
  export { RequirementIntakeStore, newIntakeId, type RequirementIntakeStoreOptions, type IntakeIndexEntry, type StoreUpdateOptions, type StoreCreateResult, } from './store.js';
19
19
  export { llmSuggestionOutputSchema, validateLlmSuggestionOutput, toProposals, assertSuggestionString, type LlmSuggestionRequest, type LlmSuggestionOutput, type LlmSuggestionGenerator, type NormalizedLlmSuggestion, } from './suggestions.js';
20
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';
21
22
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -878,8 +878,45 @@ function assertSuggestionString(value, label, max) {
878
878
  return value;
879
879
  }
880
880
 
881
- // src/service.ts
881
+ // src/service-helpers.ts
882
882
  import { ulid as ulid4 } from "@wrongstack/core/utils";
883
+
884
+ // src/vibe.ts
885
+ var VIBE_TAG_REGEX = /\[VIBE\]/i;
886
+ function hasVibeTag(text) {
887
+ if (!text) return false;
888
+ return VIBE_TAG_REGEX.test(text);
889
+ }
890
+ function stripVibeTag(text) {
891
+ return text.replace(VIBE_TAG_REGEX, "").replace(/[ \t]{2,}/g, " ").trim();
892
+ }
893
+ function deriveVibeState(rawText, existingState, now = Date.now()) {
894
+ const containsTag = hasVibeTag(rawText);
895
+ if (existingState?.isVibeMode) {
896
+ return {
897
+ ...existingState,
898
+ isVibeMode: true
899
+ };
900
+ }
901
+ if (containsTag) {
902
+ return {
903
+ isVibeMode: true,
904
+ detectedAt: now,
905
+ stage: "synthesizer"
906
+ };
907
+ }
908
+ return void 0;
909
+ }
910
+
911
+ // src/service-helpers.ts
912
+ function appendItems(target, raw) {
913
+ const items = raw.split(/[\n,;]+/).map((item) => item.trim()).filter((item) => item.length > 0);
914
+ for (const item of items) {
915
+ if (!target.includes(item) && target.length < MAX_ARRAY_ITEMS) {
916
+ target.push(item);
917
+ }
918
+ }
919
+ }
883
920
  var ANSWER_FIELD_MAPPING = {
884
921
  business_goal: {
885
922
  set: (record, value) => {
@@ -931,11 +968,294 @@ var ANSWER_FIELD_MAPPING = {
931
968
  }
932
969
  }
933
970
  };
934
- function appendItems(target, value) {
935
- const items = value.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
936
- const remaining = Math.max(0, MAX_ARRAY_ITEMS - target.length);
937
- target.push(...items.slice(0, remaining));
971
+ function buildNewIntakeRecord(input, ctx, now, catalog) {
972
+ const titleProvided = input.title !== void 0 && input.title.trim().length > 0;
973
+ const title = titleProvided ? input.title.trim() : deterministicTitle(input.originalRequest);
974
+ const requestType = normalizeRequestType(input.requestType);
975
+ const idempotencyKey = input.idempotencyKey?.trim();
976
+ const attachments = (input.attachments ?? []).map((attachment) => ({
977
+ id: `${ATTACHMENT_ID_PREFIX}${ulid4()}`,
978
+ name: attachment.name,
979
+ kind: attachment.kind,
980
+ path: attachment.path,
981
+ url: attachment.url,
982
+ sizeBytes: attachment.sizeBytes,
983
+ mimeType: attachment.mimeType,
984
+ source: "user",
985
+ addedBy: ctx.id,
986
+ addedAt: now
987
+ }));
988
+ const relatedResources = (input.relatedResources ?? []).map((resource) => ({
989
+ id: `${RELATED_RESOURCE_ID_PREFIX}${ulid4()}`,
990
+ kind: resource.kind,
991
+ reference: resource.reference,
992
+ title: resource.title,
993
+ source: "user",
994
+ addedBy: ctx.id,
995
+ addedAt: now
996
+ }));
997
+ const vibeState = deriveVibeState(
998
+ input.originalRequest,
999
+ input.vibeProtocol ?? (input.isVibeMode ? { isVibeMode: true, detectedAt: now, stage: "synthesizer" } : void 0),
1000
+ now
1001
+ );
1002
+ return {
1003
+ id: newIntakeId(),
1004
+ projectId: input.projectId,
1005
+ title,
1006
+ originalRequest: input.originalRequest,
1007
+ normalizedSummary: deterministicSummary(input.originalRequest),
1008
+ requestType,
1009
+ status: "draft",
1010
+ priority: input.priority ?? "unspecified",
1011
+ requestedBy: input.requestedBy,
1012
+ ...vibeState ? { isVibeMode: true, vibeProtocol: vibeState } : {},
1013
+ ...input.businessGoal !== void 0 ? { businessGoal: input.businessGoal } : {},
1014
+ targetUsers: [...input.targetUsers ?? []],
1015
+ ...input.expectedOutcome !== void 0 ? { expectedOutcome: input.expectedOutcome } : {},
1016
+ ...input.scopeNotes !== void 0 ? { scopeNotes: input.scopeNotes } : {},
1017
+ constraints: [...input.constraints ?? []],
1018
+ providedContext: [...input.providedContext ?? []],
1019
+ attachments,
1020
+ relatedResources,
1021
+ answers: [],
1022
+ questions: buildInitialQuestions(input, catalog),
1023
+ llmSuggestions: [],
1024
+ metadata: { ...input.metadata ?? {} },
1025
+ fieldSources: {
1026
+ ...titleProvided ? { title: "user" } : { title: "deterministic" },
1027
+ normalized_summary: "deterministic",
1028
+ request_type: input.requestType !== void 0 ? "user" : "deterministic",
1029
+ priority: input.priority !== void 0 ? "user" : "deterministic",
1030
+ ...input.businessGoal !== void 0 ? { business_goal: "user" } : {},
1031
+ ...input.targetUsers !== void 0 ? { target_users: "user" } : {},
1032
+ ...input.expectedOutcome !== void 0 ? { expected_outcome: "user" } : {},
1033
+ ...input.scopeNotes !== void 0 ? { scope_notes: "user" } : {},
1034
+ ...input.constraints !== void 0 ? { constraints: "user" } : {},
1035
+ ...input.providedContext !== void 0 ? { provided_context: "user" } : {},
1036
+ ...attachments.length > 0 ? { attachments: "user" } : {},
1037
+ ...relatedResources.length > 0 ? { related_resources: "user" } : {}
1038
+ },
1039
+ ...idempotencyKey !== void 0 && idempotencyKey.length > 0 ? { idempotencyKey } : {},
1040
+ version: 1,
1041
+ history: [{ at: now, actor: ctx.id, actorType: ctx.type, action: "created" }],
1042
+ createdAt: now,
1043
+ updatedAt: now
1044
+ };
1045
+ }
1046
+ function assertIntakeSubmitReady(record) {
1047
+ const issues = [];
1048
+ if (record.originalRequest.trim().length === 0) {
1049
+ issues.push({ field: "originalRequest", message: "original request must not be empty" });
1050
+ }
1051
+ if (record.title.trim().length === 0) {
1052
+ issues.push({ field: "title", message: "title must not be empty" });
1053
+ }
1054
+ if (record.requestedBy.trim().length === 0) {
1055
+ issues.push({ field: "requestedBy", message: "requester must not be empty" });
1056
+ }
1057
+ if (record.projectId.trim().length === 0) {
1058
+ issues.push({ field: "projectId", message: "project must not be empty" });
1059
+ }
1060
+ if (issues.length > 0) {
1061
+ throw new IntakeValidationError(issues, "Requirement intake is not ready for submission");
1062
+ }
1063
+ }
1064
+ function findSuggestionProposal(record, proposalId) {
1065
+ const proposal2 = record.llmSuggestions.find((candidate) => candidate.id === proposalId);
1066
+ if (!proposal2) {
1067
+ throw new IntakeValidationError([
1068
+ { field: "suggestionId", message: `suggestion not found: ${proposalId}` }
1069
+ ]);
1070
+ }
1071
+ return proposal2;
1072
+ }
1073
+ function applySuggestionProposal(record, proposal2) {
1074
+ switch (proposal2.kind) {
1075
+ case "title": {
1076
+ const value = assertSuggestionString(proposal2.value, "title", MAX_TITLE_LENGTH);
1077
+ record.title = value;
1078
+ record.fieldSources.title = "llm";
1079
+ break;
1080
+ }
1081
+ case "summary": {
1082
+ const value = assertSuggestionString(
1083
+ proposal2.value,
1084
+ "normalized_summary",
1085
+ MAX_SUMMARY_LENGTH
1086
+ );
1087
+ record.normalizedSummary = value;
1088
+ record.fieldSources.normalized_summary = "llm";
1089
+ break;
1090
+ }
1091
+ case "request_type": {
1092
+ const value = normalizeRequestType(proposal2.value);
1093
+ record.requestType = value;
1094
+ record.fieldSources.request_type = "llm";
1095
+ break;
1096
+ }
1097
+ case "priority": {
1098
+ const value = String(proposal2.value).trim().toLowerCase();
1099
+ if (INTAKE_PRIORITIES.includes(value)) {
1100
+ record.priority = value;
1101
+ record.fieldSources.priority = "llm";
1102
+ }
1103
+ break;
1104
+ }
1105
+ case "constraint": {
1106
+ const value = assertSuggestionString(proposal2.value, "constraint", MAX_STRING_FIELD_LENGTH);
1107
+ appendItems(record.constraints, value);
1108
+ record.fieldSources.constraints = "llm";
1109
+ break;
1110
+ }
1111
+ case "target_user": {
1112
+ const value = assertSuggestionString(
1113
+ proposal2.value,
1114
+ "target_user",
1115
+ MAX_STRING_FIELD_LENGTH
1116
+ );
1117
+ appendItems(record.targetUsers, value);
1118
+ record.fieldSources.target_users = "llm";
1119
+ break;
1120
+ }
1121
+ case "outcome": {
1122
+ const value = assertSuggestionString(proposal2.value, "outcome", MAX_STRING_FIELD_LENGTH);
1123
+ record.expectedOutcome = value;
1124
+ record.fieldSources.expected_outcome = "llm";
1125
+ break;
1126
+ }
1127
+ case "question": {
1128
+ const template = proposal2.value;
1129
+ if (typeof template === "object" && template !== null && typeof template.field === "string" && typeof template.question === "string") {
1130
+ upsertQuestion(record, {
1131
+ field: template.field,
1132
+ question: template.question,
1133
+ required: template.required
1134
+ });
1135
+ }
1136
+ break;
1137
+ }
1138
+ }
1139
+ }
1140
+ function applyOptionalString(record, field, value) {
1141
+ if (value === void 0) return;
1142
+ const trimmed = value.trim();
1143
+ if (trimmed.length === 0) {
1144
+ delete record[field];
1145
+ } else {
1146
+ record[field] = trimmed;
1147
+ }
1148
+ }
1149
+ function markUserSources(record, changedKeys) {
1150
+ const mapping = {
1151
+ title: "title",
1152
+ requestType: "request_type",
1153
+ priority: "priority",
1154
+ businessGoal: "business_goal",
1155
+ targetUsers: "target_users",
1156
+ expectedOutcome: "expected_outcome",
1157
+ scopeNotes: "scope_notes",
1158
+ constraints: "constraints",
1159
+ providedContext: "provided_context"
1160
+ };
1161
+ for (const key of changedKeys) {
1162
+ const sourceField = mapping[key];
1163
+ if (sourceField) {
1164
+ record.fieldSources[sourceField] = "user";
1165
+ }
1166
+ }
1167
+ }
1168
+ function markQuestionAnswered(record, field, value) {
1169
+ const question = record.questions.find((candidate) => candidate.field === field);
1170
+ if (question && question.status === "unanswered") {
1171
+ question.status = "answered";
1172
+ question.answer = value;
1173
+ }
1174
+ }
1175
+ function applyAnswerToRecord(record, validated, actorId, now) {
1176
+ const matchingQuestion = record.questions.find((candidate) => candidate.field === validated.field);
1177
+ const answer = {
1178
+ id: `${ANSWER_ID_PREFIX}${ulid4()}`,
1179
+ field: validated.field,
1180
+ question: validated.question ?? matchingQuestion?.question ?? validated.field,
1181
+ answer: validated.answer,
1182
+ answeredBy: actorId,
1183
+ answeredAt: now,
1184
+ source: "user"
1185
+ };
1186
+ record.answers.push(answer);
1187
+ markQuestionAnswered(record, validated.field, validated.answer);
1188
+ const mapping = ANSWER_FIELD_MAPPING[validated.field];
1189
+ if (mapping) {
1190
+ mapping.set(record, validated.answer);
1191
+ const sourceKey = validated.field;
1192
+ record.fieldSources[sourceKey] = "user";
1193
+ }
1194
+ return answer;
1195
+ }
1196
+ function applyAnswerUpdateToRecord(record, answerId, newAnswer) {
1197
+ const answer = record.answers.find((candidate) => candidate.id === answerId);
1198
+ if (!answer) {
1199
+ throw new IntakeValidationError([
1200
+ { field: "answerId", message: `answer not found: ${answerId}` }
1201
+ ]);
1202
+ }
1203
+ answer.answer = newAnswer;
1204
+ answer.answeredAt = Date.now();
1205
+ const question = record.questions.find((candidate) => candidate.field === answer.field);
1206
+ if (question) {
1207
+ question.answer = newAnswer;
1208
+ question.status = "answered";
1209
+ }
938
1210
  }
1211
+ function applyAttachmentToRecord(record, validated, actorId, now) {
1212
+ if (record.attachments.length >= MAX_ATTACHMENTS) {
1213
+ throw new IntakeValidationError([
1214
+ { field: "attachments", message: `maximum of ${MAX_ATTACHMENTS} attachments reached` }
1215
+ ]);
1216
+ }
1217
+ const attachment = {
1218
+ id: `${ATTACHMENT_ID_PREFIX}${ulid4()}`,
1219
+ name: validated.attachment.name,
1220
+ kind: validated.attachment.kind,
1221
+ path: validated.attachment?.path,
1222
+ url: validated.attachment?.url,
1223
+ sizeBytes: validated.attachment?.sizeBytes,
1224
+ mimeType: validated.attachment?.mimeType,
1225
+ source: "user",
1226
+ addedBy: actorId,
1227
+ addedAt: now
1228
+ };
1229
+ record.attachments.push(attachment);
1230
+ record.fieldSources.attachments = "user";
1231
+ markQuestionAnswered(record, "attachments", attachment.name);
1232
+ return attachment;
1233
+ }
1234
+ function applyRelatedResourceToRecord(record, validated, actorId, now) {
1235
+ if (record.relatedResources.length >= MAX_RELATED_RESOURCES) {
1236
+ throw new IntakeValidationError([
1237
+ {
1238
+ field: "relatedResources",
1239
+ message: `maximum of ${MAX_RELATED_RESOURCES} related resources reached`
1240
+ }
1241
+ ]);
1242
+ }
1243
+ const resource = {
1244
+ id: `${RELATED_RESOURCE_ID_PREFIX}${ulid4()}`,
1245
+ kind: validated.relatedResource.kind,
1246
+ reference: validated.relatedResource.reference,
1247
+ title: validated.relatedResource?.title,
1248
+ source: "user",
1249
+ addedBy: actorId,
1250
+ addedAt: now
1251
+ };
1252
+ record.relatedResources.push(resource);
1253
+ record.fieldSources.related_resources = "user";
1254
+ markQuestionAnswered(record, "related_resources", resource.reference);
1255
+ return resource;
1256
+ }
1257
+
1258
+ // src/service.ts
939
1259
  var RequirementIntakeService = class {
940
1260
  store;
941
1261
  authorizer;
@@ -1057,6 +1377,8 @@ var RequirementIntakeService = class {
1057
1377
  if (validated.providedContext !== void 0)
1058
1378
  next.providedContext = [...validated.providedContext];
1059
1379
  if (validated.metadata !== void 0) next.metadata = validated.metadata;
1380
+ if (validated.isVibeMode !== void 0) next.isVibeMode = validated.isVibeMode;
1381
+ if (validated.vibeProtocol !== void 0) next.vibeProtocol = validated.vibeProtocol;
1060
1382
  markUserSources(next, changedKeys);
1061
1383
  }).then((updated) => {
1062
1384
  this.afterMutation(updated, ctx, "RequirementIntakeUpdated");
@@ -1072,25 +1394,7 @@ var RequirementIntakeService = class {
1072
1394
  id,
1073
1395
  this.updateMeta(ctx, "answer_added", [validated.field], expectedVersion),
1074
1396
  (next) => {
1075
- const question = next.questions.find((candidate) => candidate.field === validated.field);
1076
- const answer = {
1077
- id: `${ANSWER_ID_PREFIX}${ulid4()}`,
1078
- field: validated.field,
1079
- question: validated.question ?? question?.question ?? validated.field,
1080
- answer: validated.answer,
1081
- source: "user",
1082
- answeredBy: ctx.id,
1083
- answeredAt: Date.now()
1084
- };
1085
- next.answers.push(answer);
1086
- if (question && question.status === "unanswered") {
1087
- question.status = "answered";
1088
- question.answer = validated.answer;
1089
- }
1090
- ANSWER_FIELD_MAPPING[validated.field]?.set(next, validated.answer);
1091
- if (INTAKE_FIELDS.includes(validated.field)) {
1092
- next.fieldSources[validated.field] = "user";
1093
- }
1397
+ applyAnswerToRecord(next, validated, ctx.id, Date.now());
1094
1398
  }
1095
1399
  ).then((updated) => {
1096
1400
  this.afterMutation(updated, ctx, "RequirementIntakeUpdated");
@@ -1107,19 +1411,7 @@ var RequirementIntakeService = class {
1107
1411
  })
1108
1412
  );
1109
1413
  return this.store.update(id, this.updateMeta(ctx, "answer_updated", [answerId], expectedVersion), (next) => {
1110
- const answer = next.answers.find((candidate) => candidate.id === answerId);
1111
- if (!answer) {
1112
- throw new IntakeValidationError([
1113
- { field: "answerId", message: `answer not found: ${answerId}` }
1114
- ]);
1115
- }
1116
- answer.answer = validated.answer;
1117
- answer.answeredAt = Date.now();
1118
- const question = next.questions.find((candidate) => candidate.field === answer.field);
1119
- if (question) {
1120
- question.answer = validated.answer;
1121
- question.status = "answered";
1122
- }
1414
+ applyAnswerUpdateToRecord(next, answerId, validated.answer);
1123
1415
  }).then((updated) => {
1124
1416
  this.afterMutation(updated, ctx, "RequirementIntakeUpdated");
1125
1417
  return updated;
@@ -1131,60 +1423,22 @@ var RequirementIntakeService = class {
1131
1423
  const validated = this.guardValidation(() => validateAttachResourceInput(input));
1132
1424
  const now = Date.now();
1133
1425
  if (validated.attachment !== void 0) {
1134
- if (record.attachments.length >= MAX_ATTACHMENTS) {
1135
- throw new IntakeValidationError([
1136
- { field: "attachments", message: `maximum of ${MAX_ATTACHMENTS} attachments reached` }
1137
- ]);
1138
- }
1139
1426
  return this.store.update(
1140
1427
  id,
1141
1428
  this.updateMeta(ctx, "attachment_added", ["attachments"], expectedVersion),
1142
1429
  (next) => {
1143
- const attachment = {
1144
- id: `${ATTACHMENT_ID_PREFIX}${ulid4()}`,
1145
- name: validated.attachment.name,
1146
- kind: validated.attachment.kind,
1147
- path: validated.attachment?.path,
1148
- url: validated.attachment?.url,
1149
- sizeBytes: validated.attachment?.sizeBytes,
1150
- mimeType: validated.attachment?.mimeType,
1151
- source: "user",
1152
- addedBy: ctx.id,
1153
- addedAt: now
1154
- };
1155
- next.attachments.push(attachment);
1156
- next.fieldSources.attachments = "user";
1157
- markQuestionAnswered(next, "attachments", attachment.name);
1430
+ applyAttachmentToRecord(next, validated, ctx.id, now);
1158
1431
  }
1159
1432
  ).then((updated) => {
1160
1433
  this.afterMutation(updated, ctx, "RequirementIntakeUpdated");
1161
1434
  return updated;
1162
1435
  });
1163
1436
  }
1164
- if (record.relatedResources.length >= MAX_RELATED_RESOURCES) {
1165
- throw new IntakeValidationError([
1166
- {
1167
- field: "relatedResources",
1168
- message: `maximum of ${MAX_RELATED_RESOURCES} related resources reached`
1169
- }
1170
- ]);
1171
- }
1172
1437
  return this.store.update(
1173
1438
  id,
1174
1439
  this.updateMeta(ctx, "related_resource_added", ["related_resources"], expectedVersion),
1175
1440
  (next) => {
1176
- const resource = {
1177
- id: `${RELATED_RESOURCE_ID_PREFIX}${ulid4()}`,
1178
- kind: validated.relatedResource.kind,
1179
- reference: validated.relatedResource.reference,
1180
- title: validated.relatedResource?.title,
1181
- source: "user",
1182
- addedBy: ctx.id,
1183
- addedAt: now
1184
- };
1185
- next.relatedResources.push(resource);
1186
- next.fieldSources.related_resources = "user";
1187
- markQuestionAnswered(next, "related_resources", resource.reference);
1441
+ applyRelatedResourceToRecord(next, validated, ctx.id, now);
1188
1442
  }
1189
1443
  ).then((updated) => {
1190
1444
  this.afterMutation(updated, ctx, "RequirementIntakeUpdated");
@@ -1452,73 +1706,7 @@ var RequirementIntakeService = class {
1452
1706
  // Internals
1453
1707
  // -------------------------------------------------------------------------
1454
1708
  buildNewRecord(input, ctx, now) {
1455
- const titleProvided = input.title !== void 0 && input.title.trim().length > 0;
1456
- const title = titleProvided ? input.title.trim() : deterministicTitle(input.originalRequest);
1457
- const requestType = normalizeRequestType(input.requestType);
1458
- const idempotencyKey = input.idempotencyKey?.trim();
1459
- const attachments = (input.attachments ?? []).map((attachment) => ({
1460
- id: `${ATTACHMENT_ID_PREFIX}${ulid4()}`,
1461
- name: attachment.name,
1462
- kind: attachment.kind,
1463
- path: attachment.path,
1464
- url: attachment.url,
1465
- sizeBytes: attachment.sizeBytes,
1466
- mimeType: attachment.mimeType,
1467
- source: "user",
1468
- addedBy: ctx.id,
1469
- addedAt: now
1470
- }));
1471
- const relatedResources = (input.relatedResources ?? []).map((resource) => ({
1472
- id: `${RELATED_RESOURCE_ID_PREFIX}${ulid4()}`,
1473
- kind: resource.kind,
1474
- reference: resource.reference,
1475
- title: resource.title,
1476
- source: "user",
1477
- addedBy: ctx.id,
1478
- addedAt: now
1479
- }));
1480
- return {
1481
- id: newIntakeId(),
1482
- projectId: input.projectId,
1483
- title,
1484
- originalRequest: input.originalRequest,
1485
- normalizedSummary: deterministicSummary(input.originalRequest),
1486
- requestType,
1487
- status: "draft",
1488
- priority: input.priority ?? "unspecified",
1489
- requestedBy: input.requestedBy,
1490
- ...input.businessGoal !== void 0 ? { businessGoal: input.businessGoal } : {},
1491
- targetUsers: [...input.targetUsers ?? []],
1492
- ...input.expectedOutcome !== void 0 ? { expectedOutcome: input.expectedOutcome } : {},
1493
- ...input.scopeNotes !== void 0 ? { scopeNotes: input.scopeNotes } : {},
1494
- constraints: [...input.constraints ?? []],
1495
- providedContext: [...input.providedContext ?? []],
1496
- attachments,
1497
- relatedResources,
1498
- answers: [],
1499
- questions: buildInitialQuestions(input, this.catalog),
1500
- llmSuggestions: [],
1501
- metadata: { ...input.metadata ?? {} },
1502
- fieldSources: {
1503
- ...titleProvided ? { title: "user" } : { title: "deterministic" },
1504
- normalized_summary: "deterministic",
1505
- request_type: input.requestType !== void 0 ? "user" : "deterministic",
1506
- priority: input.priority !== void 0 ? "user" : "deterministic",
1507
- ...input.businessGoal !== void 0 ? { business_goal: "user" } : {},
1508
- ...input.targetUsers !== void 0 ? { target_users: "user" } : {},
1509
- ...input.expectedOutcome !== void 0 ? { expected_outcome: "user" } : {},
1510
- ...input.scopeNotes !== void 0 ? { scope_notes: "user" } : {},
1511
- ...input.constraints !== void 0 ? { constraints: "user" } : {},
1512
- ...input.providedContext !== void 0 ? { provided_context: "user" } : {},
1513
- ...attachments.length > 0 ? { attachments: "user" } : {},
1514
- ...relatedResources.length > 0 ? { related_resources: "user" } : {}
1515
- },
1516
- ...idempotencyKey !== void 0 && idempotencyKey.length > 0 ? { idempotencyKey } : {},
1517
- version: 1,
1518
- history: [{ at: now, actor: ctx.id, actorType: ctx.type, action: "created" }],
1519
- createdAt: now,
1520
- updatedAt: now
1521
- };
1709
+ return buildNewIntakeRecord(input, ctx, now, this.catalog);
1522
1710
  }
1523
1711
  async requireRecord(id, ctx, operation) {
1524
1712
  const record = await this.store.load(id);
@@ -1547,99 +1735,20 @@ var RequirementIntakeService = class {
1547
1735
  }
1548
1736
  }
1549
1737
  assertSubmitReady(record) {
1550
- const issues = [];
1551
- if (record.originalRequest.trim().length === 0) {
1552
- issues.push({ field: "originalRequest", message: "original request must not be empty" });
1553
- }
1554
- if (record.title.trim().length === 0) {
1555
- issues.push({ field: "title", message: "title must not be empty" });
1556
- }
1557
- if (record.requestedBy.trim().length === 0) {
1558
- issues.push({ field: "requestedBy", message: "requester must not be empty" });
1559
- }
1560
- if (record.projectId.trim().length === 0) {
1561
- issues.push({ field: "projectId", message: "project must not be empty" });
1562
- }
1563
- if (issues.length > 0) {
1564
- this.metrics.increment("intake.validation_failure");
1565
- throw new IntakeValidationError(issues, "Requirement intake is not ready for submission");
1738
+ try {
1739
+ assertIntakeSubmitReady(record);
1740
+ } catch (error) {
1741
+ if (error instanceof IntakeValidationError) {
1742
+ this.metrics.increment("intake.validation_failure");
1743
+ }
1744
+ throw error;
1566
1745
  }
1567
1746
  }
1568
1747
  findSuggestion(record, proposalId) {
1569
- const proposal2 = record.llmSuggestions.find((candidate) => candidate.id === proposalId);
1570
- if (!proposal2) {
1571
- throw new IntakeValidationError([
1572
- { field: "suggestionId", message: `suggestion not found: ${proposalId}` }
1573
- ]);
1574
- }
1575
- return proposal2;
1748
+ return findSuggestionProposal(record, proposalId);
1576
1749
  }
1577
1750
  applyProposal(record, proposal2) {
1578
- switch (proposal2.kind) {
1579
- case "title": {
1580
- const value = assertSuggestionString(proposal2.value, "title", MAX_TITLE_LENGTH);
1581
- record.title = value;
1582
- record.fieldSources.title = "llm";
1583
- break;
1584
- }
1585
- case "summary": {
1586
- const value = assertSuggestionString(
1587
- proposal2.value,
1588
- "normalized_summary",
1589
- MAX_SUMMARY_LENGTH
1590
- );
1591
- record.normalizedSummary = value;
1592
- record.fieldSources.normalized_summary = "llm";
1593
- break;
1594
- }
1595
- case "request_type": {
1596
- const value = normalizeRequestType(proposal2.value);
1597
- record.requestType = value;
1598
- record.fieldSources.request_type = "llm";
1599
- break;
1600
- }
1601
- case "priority": {
1602
- const value = String(proposal2.value).trim().toLowerCase();
1603
- if (INTAKE_PRIORITIES.includes(value)) {
1604
- record.priority = value;
1605
- record.fieldSources.priority = "llm";
1606
- }
1607
- break;
1608
- }
1609
- case "constraint": {
1610
- const value = assertSuggestionString(proposal2.value, "constraint", MAX_STRING_FIELD_LENGTH);
1611
- appendItems(record.constraints, value);
1612
- record.fieldSources.constraints = "llm";
1613
- break;
1614
- }
1615
- case "target_user": {
1616
- const value = assertSuggestionString(
1617
- proposal2.value,
1618
- "target_user",
1619
- MAX_STRING_FIELD_LENGTH
1620
- );
1621
- appendItems(record.targetUsers, value);
1622
- record.fieldSources.target_users = "llm";
1623
- break;
1624
- }
1625
- case "outcome": {
1626
- const value = assertSuggestionString(proposal2.value, "outcome", MAX_STRING_FIELD_LENGTH);
1627
- record.expectedOutcome = value;
1628
- record.fieldSources.expected_outcome = "llm";
1629
- break;
1630
- }
1631
- case "question": {
1632
- const template = proposal2.value;
1633
- if (typeof template === "object" && template !== null && typeof template.field === "string" && typeof template.question === "string") {
1634
- upsertQuestion(record, {
1635
- field: template.field,
1636
- question: template.question,
1637
- required: template.required
1638
- });
1639
- }
1640
- break;
1641
- }
1642
- }
1751
+ applySuggestionProposal(record, proposal2);
1643
1752
  }
1644
1753
  updateMeta(ctx, action, fields, expectedVersion) {
1645
1754
  return {
@@ -1678,41 +1787,6 @@ var RequirementIntakeService = class {
1678
1787
  }
1679
1788
  }
1680
1789
  };
1681
- function applyOptionalString(record, field, value) {
1682
- if (value === void 0) return;
1683
- const trimmed = value.trim();
1684
- if (trimmed.length === 0) {
1685
- delete record[field];
1686
- } else {
1687
- record[field] = trimmed;
1688
- }
1689
- }
1690
- function markUserSources(record, changedKeys) {
1691
- const mapping = {
1692
- title: "title",
1693
- requestType: "request_type",
1694
- priority: "priority",
1695
- businessGoal: "business_goal",
1696
- targetUsers: "target_users",
1697
- expectedOutcome: "expected_outcome",
1698
- scopeNotes: "scope_notes",
1699
- constraints: "constraints",
1700
- providedContext: "provided_context"
1701
- };
1702
- for (const key of changedKeys) {
1703
- const sourceField = mapping[key];
1704
- if (sourceField) {
1705
- record.fieldSources[sourceField] = "user";
1706
- }
1707
- }
1708
- }
1709
- function markQuestionAnswered(record, field, value) {
1710
- const question = record.questions.find((candidate) => candidate.field === field);
1711
- if (question && question.status === "unanswered") {
1712
- question.status = "answered";
1713
- question.answer = value;
1714
- }
1715
- }
1716
1790
  export {
1717
1791
  ALLOWED_TRANSITIONS,
1718
1792
  AllowAllIntakeAuthorizer,
@@ -1765,6 +1839,7 @@ export {
1765
1839
  RequirementIntakeStore,
1766
1840
  SUGGESTION_KINDS,
1767
1841
  SUGGESTION_STATUSES,
1842
+ VIBE_TAG_REGEX,
1768
1843
  answerInputSchema,
1769
1844
  assertSuggestionString,
1770
1845
  assertTransition,
@@ -1773,8 +1848,10 @@ export {
1773
1848
  buildInitialQuestions,
1774
1849
  canTransition,
1775
1850
  createIntakeSchema,
1851
+ deriveVibeState,
1776
1852
  deterministicSummary,
1777
1853
  deterministicTitle,
1854
+ hasVibeTag,
1778
1855
  isBlank,
1779
1856
  isKnownStatus,
1780
1857
  isMutableStatus,
@@ -1789,6 +1866,7 @@ export {
1789
1866
  questionTemplateInputSchema,
1790
1867
  relatedResourceInputSchema,
1791
1868
  requestTypeSchema,
1869
+ stripVibeTag,
1792
1870
  toProposals,
1793
1871
  updateIntakeSchema,
1794
1872
  upsertQuestion,
@@ -0,0 +1,19 @@
1
+ import { type IntakeQuestionTemplate } from './constants.js';
2
+ import type { AddAnswerInput, AttachResourceInput, CreateIntakeInput, IntakeAnswer, IntakeAttachment, IntakeContext, LlmSuggestionProposal, RelatedResource, RequirementIntakeRecord } from './types.js';
3
+ export declare function appendItems(target: string[], raw: string): void;
4
+ /** Answer fields that also update a record property. */
5
+ export declare const ANSWER_FIELD_MAPPING: Readonly<Record<string, {
6
+ set: (record: RequirementIntakeRecord, value: string) => void;
7
+ }>>;
8
+ export declare function buildNewIntakeRecord(input: CreateIntakeInput, ctx: IntakeContext, now: number, catalog: readonly IntakeQuestionTemplate[]): RequirementIntakeRecord;
9
+ export declare function assertIntakeSubmitReady(record: RequirementIntakeRecord): void;
10
+ export declare function findSuggestionProposal(record: RequirementIntakeRecord, proposalId: string): LlmSuggestionProposal;
11
+ export declare function applySuggestionProposal(record: RequirementIntakeRecord, proposal: LlmSuggestionProposal): void;
12
+ export declare function applyOptionalString(record: RequirementIntakeRecord, field: 'businessGoal' | 'expectedOutcome' | 'scopeNotes', value: string | undefined): void;
13
+ export declare function markUserSources(record: RequirementIntakeRecord, changedKeys: string[]): void;
14
+ export declare function markQuestionAnswered(record: RequirementIntakeRecord, field: string, value: string): void;
15
+ export declare function applyAnswerToRecord(record: RequirementIntakeRecord, validated: AddAnswerInput, actorId: string, now: number): IntakeAnswer;
16
+ export declare function applyAnswerUpdateToRecord(record: RequirementIntakeRecord, answerId: string, newAnswer: string): void;
17
+ export declare function applyAttachmentToRecord(record: RequirementIntakeRecord, validated: AttachResourceInput, actorId: string, now: number): IntakeAttachment;
18
+ export declare function applyRelatedResourceToRecord(record: RequirementIntakeRecord, validated: AttachResourceInput, actorId: string, now: number): RelatedResource;
19
+ //# sourceMappingURL=service-helpers.d.ts.map
package/dist/service.d.ts CHANGED
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Requirements Intake — service.
3
+ *
4
+ * The single entry point for all intake operations. Enforces:
5
+ * - deterministic validation of every input (untrusted text stays data),
6
+ * - authorization on every operation (fail closed),
7
+ * - lifecycle transitions via application logic only,
8
+ * - optimistic concurrency (expectedVersion),
9
+ * - idempotent create (idempotency key) and submit,
10
+ * - source tracking (user / llm / deterministic),
11
+ * - domain events + structured logs + metrics with safe fields only.
12
+ *
13
+ * The original request is immutable after creation: no API path can change
14
+ * `originalRequest`, and LLM suggestions can never write to it.
15
+ */
1
16
  import { type IntakeQuestionTemplate, type IntakeStatus } from './constants.js';
2
17
  import type { IntakeAuthorizer } from './authorization.js';
3
18
  import { IntakeEventEmitter } from './events.js';
package/dist/types.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * generated value separate, source-annotated, and user-editable.
7
7
  */
8
8
  import type { IntakeAttachmentKind, IntakeField, IntakeFieldSource, IntakePriority, IntakeQuestionStatus, IntakeStatus, RelatedResourceKind, RequestType, SuggestionKind, SuggestionStatus } from './constants.js';
9
+ import type { VibeProtocolState } from './vibe.js';
9
10
  /** Actor identity used for authorization and audit. */
10
11
  export interface IntakeActor {
11
12
  /** Stable actor id (user id, agent id, or automation id). */
@@ -116,6 +117,10 @@ export interface RequirementIntakeRecord {
116
117
  questions: IntakeQuestion[];
117
118
  llmSuggestions: LlmSuggestionProposal[];
118
119
  metadata: Record<string, unknown>;
120
+ /** True when the request is processed under the Three-Stage VIBE Verification Protocol. */
121
+ isVibeMode?: boolean | undefined;
122
+ /** Persisted state of the VIBE protocol pipeline (Spec-Synthesizer -> Coder -> Auditor). */
123
+ vibeProtocol?: VibeProtocolState | undefined;
119
124
  /** Source annotation per record field ('user' | 'llm' | 'deterministic'). */
120
125
  fieldSources: Partial<Record<IntakeField, IntakeFieldSource>>;
121
126
  /** Raw idempotency key used at creation (kept for diagnostics). */
@@ -151,6 +156,10 @@ export interface CreateIntakeInput {
151
156
  attachments?: IntakeAttachmentInput[] | undefined;
152
157
  relatedResources?: RelatedResourceInput[] | undefined;
153
158
  metadata?: Record<string, unknown> | undefined;
159
+ /** Explicit override for Vibe Mode; if omitted, automatically detected from originalRequest. */
160
+ isVibeMode?: boolean | undefined;
161
+ /** Optional initial vibe state if resuming an existing cycle. */
162
+ vibeProtocol?: VibeProtocolState | undefined;
154
163
  /** Idempotent-create key: same key + project returns the existing record. */
155
164
  idempotencyKey?: string | undefined;
156
165
  /**
@@ -191,6 +200,8 @@ export interface UpdateIntakeInput {
191
200
  constraints?: string[] | undefined;
192
201
  providedContext?: string[] | undefined;
193
202
  metadata?: Record<string, unknown> | undefined;
203
+ isVibeMode?: boolean | undefined;
204
+ vibeProtocol?: VibeProtocolState | undefined;
194
205
  }
195
206
  /** Payload for answering an intake question. */
196
207
  export interface AddAnswerInput {
package/dist/vibe.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Requirements Intake — VIBE Three-Stage Verification Protocol.
3
+ *
4
+ * Provides detection, tag parsing, and persistent state management for requests
5
+ * marked with the `[VIBE]` protocol trigger anywhere in the prompt text.
6
+ *
7
+ * When `[VIBE]` is detected, it locks the task into the Three-Stage
8
+ * Verification Protocol (Spec-Synthesizer -> Coder -> Auditor) and ensures
9
+ * the state is preserved across iterative refining and updates.
10
+ */
11
+ export declare const VIBE_TAG_REGEX: RegExp;
12
+ export type VibeProtocolStage = 'synthesizer' | 'coder' | 'auditor' | 'passed';
13
+ export interface VibeProtocolState {
14
+ /** True whenever [VIBE] was present in the prompt or preserved across refinements. */
15
+ isVibeMode: boolean;
16
+ detectedAt: number;
17
+ /** Active stage in the verification protocol. */
18
+ stage: VibeProtocolStage;
19
+ /** Structured output from Spec-Synthesizer. */
20
+ synthesizedSpec?: string | undefined;
21
+ /** Generated implementation contract or code diff. */
22
+ coderContract?: string | undefined;
23
+ /** Independent verification decision from Auditor. */
24
+ auditVerdict?: 'PASS' | 'REJECT' | undefined;
25
+ /** Detailed audit check results or revision requests. */
26
+ auditNotes?: string[] | undefined;
27
+ }
28
+ /**
29
+ * Checks if the given text contains the `[VIBE]` tag (case-insensitive)
30
+ * anywhere in the string.
31
+ */
32
+ export declare function hasVibeTag(text: string | undefined | null): boolean;
33
+ /**
34
+ * Safely strips the `[VIBE]` tag from the text for normalized display,
35
+ * preserving all other content and trimming extraneous whitespace.
36
+ */
37
+ export declare function stripVibeTag(text: string): string;
38
+ /**
39
+ * Derives or preserves Vibe Protocol state for an intake record.
40
+ * If the record was already in Vibe mode, it remains in Vibe mode across
41
+ * refinements even if subsequent messages omit the tag.
42
+ */
43
+ export declare function deriveVibeState(rawText: string, existingState?: VibeProtocolState | undefined, now?: number): VibeProtocolState | undefined;
44
+ //# sourceMappingURL=vibe.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/requirement-intake",
3
- "version": "0.306.4",
3
+ "version": "0.307.0",
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": {
@@ -28,10 +28,10 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "zod": "4.4.3",
31
- "@wrongstack/core": "0.306.4"
31
+ "@wrongstack/core": "0.307.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "^26.1.2",
34
+ "@types/node": "^26.2.0",
35
35
  "typescript": "^7.0.2"
36
36
  },
37
37
  "publishConfig": {