@wrongstack/requirement-intake 1.0.32 → 1.0.35
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/constants.d.ts +0 -2
- package/dist/index.js +157 -120
- package/dist/service-suggestions.d.ts +23 -0
- package/dist/service.d.ts +2 -1
- package/package.json +2 -2
package/dist/constants.d.ts
CHANGED
|
@@ -68,8 +68,6 @@ export declare const MAX_QUESTION_LENGTH = 500;
|
|
|
68
68
|
export declare const MAX_HISTORY_ENTRIES = 200;
|
|
69
69
|
/** Maximum number of pending LLM suggestions retained per record. */
|
|
70
70
|
export declare const MAX_SUGGESTIONS = 100;
|
|
71
|
-
/** Deterministic fallback summary length when no LLM summary is available. */
|
|
72
|
-
export declare const DETERMINISTIC_SUMMARY_LENGTH = 240;
|
|
73
71
|
/**
|
|
74
72
|
* Default intake question catalog. Hosts may override via create options.
|
|
75
73
|
* `field` values map to record properties through the answer pipeline
|
package/dist/index.js
CHANGED
|
@@ -1380,6 +1380,134 @@ function applyRelatedResourceToRecord(record, validated, actorId, now) {
|
|
|
1380
1380
|
return resource;
|
|
1381
1381
|
}
|
|
1382
1382
|
|
|
1383
|
+
// src/service-suggestions.ts
|
|
1384
|
+
async function generateSuggestions(host, id, ctx, focus) {
|
|
1385
|
+
const record = await host.requireRecord(id, ctx, "suggest");
|
|
1386
|
+
host.assertMutable(record, "generateSuggestions");
|
|
1387
|
+
if (!host.generator) {
|
|
1388
|
+
throw new IntakeSuggestionError("No LLM suggestion generator is configured on this service");
|
|
1389
|
+
}
|
|
1390
|
+
host.metrics.increment("intake.suggestions.requested");
|
|
1391
|
+
let output;
|
|
1392
|
+
try {
|
|
1393
|
+
output = await host.generator.generate({ record, focus });
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
host.metrics.increment("intake.suggestions.failed");
|
|
1396
|
+
host.logger.error("intake", "intake.suggestions.failed", {
|
|
1397
|
+
intakeId: record.id,
|
|
1398
|
+
projectId: record.projectId,
|
|
1399
|
+
actorId: ctx.id
|
|
1400
|
+
});
|
|
1401
|
+
throw new IntakeSuggestionError("LLM suggestion generation failed", { cause: error });
|
|
1402
|
+
}
|
|
1403
|
+
let proposals;
|
|
1404
|
+
try {
|
|
1405
|
+
proposals = toProposals(validateLlmSuggestionOutput(output));
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
host.metrics.increment("intake.suggestions.failed");
|
|
1408
|
+
throw error instanceof IntakeSuggestionError ? error : new IntakeSuggestionError("LLM suggestion output could not be validated", {
|
|
1409
|
+
cause: error
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
if (proposals.length === 0) {
|
|
1413
|
+
host.metrics.increment("intake.suggestions.failed");
|
|
1414
|
+
throw new IntakeSuggestionError("LLM suggestion output contained no usable proposals");
|
|
1415
|
+
}
|
|
1416
|
+
const hadQuestions = proposals.some((proposal2) => proposal2.kind === "question");
|
|
1417
|
+
let previousStatus = record.status;
|
|
1418
|
+
const updateOptions = {
|
|
1419
|
+
actorId: ctx.id,
|
|
1420
|
+
actorType: ctx.type,
|
|
1421
|
+
action: hadQuestions ? "information_requested" : "suggestions_added"
|
|
1422
|
+
};
|
|
1423
|
+
const updated = await host.store.update(id, updateOptions, (next) => {
|
|
1424
|
+
host.assertMutable(next, "generateSuggestions");
|
|
1425
|
+
previousStatus = next.status;
|
|
1426
|
+
if (next.status === "draft") {
|
|
1427
|
+
updateOptions.from = "draft";
|
|
1428
|
+
updateOptions.to = "collecting_information";
|
|
1429
|
+
next.status = "collecting_information";
|
|
1430
|
+
}
|
|
1431
|
+
next.llmSuggestions.push(...proposals);
|
|
1432
|
+
if (next.llmSuggestions.length > MAX_SUGGESTIONS) {
|
|
1433
|
+
next.llmSuggestions = next.llmSuggestions.slice(next.llmSuggestions.length - MAX_SUGGESTIONS);
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
host.metrics.increment("intake.suggestions.succeeded");
|
|
1437
|
+
host.logger.info("intake", "intake.suggestions.succeeded", {
|
|
1438
|
+
intakeId: updated.id,
|
|
1439
|
+
projectId: updated.projectId,
|
|
1440
|
+
actorId: ctx.id,
|
|
1441
|
+
count: proposals.length
|
|
1442
|
+
});
|
|
1443
|
+
host.emit(hadQuestions ? "RequirementIntakeInformationRequested" : "RequirementIntakeUpdated", {
|
|
1444
|
+
intakeId: updated.id,
|
|
1445
|
+
projectId: updated.projectId,
|
|
1446
|
+
actorId: ctx.id,
|
|
1447
|
+
actorType: ctx.type,
|
|
1448
|
+
previousStatus,
|
|
1449
|
+
status: updated.status
|
|
1450
|
+
});
|
|
1451
|
+
return proposals;
|
|
1452
|
+
}
|
|
1453
|
+
async function acceptSuggestion(host, id, proposalId, ctx, expectedVersion) {
|
|
1454
|
+
const record = await host.requireRecord(id, ctx, "accept_suggestion");
|
|
1455
|
+
host.assertMutable(record, "acceptSuggestion");
|
|
1456
|
+
const proposal2 = host.findSuggestion(record, proposalId);
|
|
1457
|
+
if (proposal2.status !== "pending") {
|
|
1458
|
+
throw new IntakeValidationError([
|
|
1459
|
+
{ field: "suggestionId", message: `suggestion is already ${proposal2.status}` }
|
|
1460
|
+
]);
|
|
1461
|
+
}
|
|
1462
|
+
return host.store.update(
|
|
1463
|
+
id,
|
|
1464
|
+
host.updateMeta(ctx, "suggestion_accepted", [proposal2.kind], expectedVersion),
|
|
1465
|
+
(next) => {
|
|
1466
|
+
host.assertMutable(next, "acceptSuggestion");
|
|
1467
|
+
const target = next.llmSuggestions.find((candidate) => candidate.id === proposalId);
|
|
1468
|
+
if (!target) {
|
|
1469
|
+
throw new IntakeValidationError([
|
|
1470
|
+
{ field: "suggestionId", message: `suggestion not found: ${proposalId}` }
|
|
1471
|
+
]);
|
|
1472
|
+
}
|
|
1473
|
+
host.applyProposal(next, target);
|
|
1474
|
+
target.status = "accepted";
|
|
1475
|
+
target.resolvedAt = Date.now();
|
|
1476
|
+
}
|
|
1477
|
+
).then((updated) => {
|
|
1478
|
+
host.afterMutation(updated, ctx, "RequirementIntakeUpdated");
|
|
1479
|
+
return updated;
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
async function rejectSuggestion(host, id, proposalId, ctx, expectedVersion) {
|
|
1483
|
+
const record = await host.requireRecord(id, ctx, "reject_suggestion");
|
|
1484
|
+
host.assertMutable(record, "rejectSuggestion");
|
|
1485
|
+
host.findSuggestion(record, proposalId);
|
|
1486
|
+
return host.store.update(
|
|
1487
|
+
id,
|
|
1488
|
+
host.updateMeta(ctx, "suggestion_rejected", [proposalId], expectedVersion),
|
|
1489
|
+
(next) => {
|
|
1490
|
+
host.assertMutable(next, "rejectSuggestion");
|
|
1491
|
+
const target = next.llmSuggestions.find((candidate) => candidate.id === proposalId);
|
|
1492
|
+
if (!target) {
|
|
1493
|
+
throw new IntakeValidationError([
|
|
1494
|
+
{ field: "suggestionId", message: `suggestion not found: ${proposalId}` }
|
|
1495
|
+
]);
|
|
1496
|
+
}
|
|
1497
|
+
if (target.status !== "pending") {
|
|
1498
|
+
throw new IntakeValidationError([
|
|
1499
|
+
{ field: "suggestionId", message: `suggestion is already ${target.status}` }
|
|
1500
|
+
]);
|
|
1501
|
+
}
|
|
1502
|
+
target.status = "rejected";
|
|
1503
|
+
target.resolvedAt = Date.now();
|
|
1504
|
+
}
|
|
1505
|
+
).then((updated) => {
|
|
1506
|
+
host.afterMutation(updated, ctx, "RequirementIntakeUpdated");
|
|
1507
|
+
return updated;
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1383
1511
|
// src/service.ts
|
|
1384
1512
|
var RequirementIntakeService = class {
|
|
1385
1513
|
store;
|
|
@@ -1598,132 +1726,25 @@ var RequirementIntakeService = class {
|
|
|
1598
1726
|
// LLM suggestions (always proposals)
|
|
1599
1727
|
// -------------------------------------------------------------------------
|
|
1600
1728
|
async generateSuggestions(id, ctx, focus) {
|
|
1601
|
-
|
|
1602
|
-
this.assertMutable(record, "generateSuggestions");
|
|
1603
|
-
if (!this.generator) {
|
|
1604
|
-
throw new IntakeSuggestionError("No LLM suggestion generator is configured on this service");
|
|
1605
|
-
}
|
|
1606
|
-
this.metrics.increment("intake.suggestions.requested");
|
|
1607
|
-
let output;
|
|
1608
|
-
try {
|
|
1609
|
-
output = await this.generator.generate({ record, focus });
|
|
1610
|
-
} catch (error) {
|
|
1611
|
-
this.metrics.increment("intake.suggestions.failed");
|
|
1612
|
-
this.logger.error("intake", "intake.suggestions.failed", {
|
|
1613
|
-
intakeId: record.id,
|
|
1614
|
-
projectId: record.projectId,
|
|
1615
|
-
actorId: ctx.id
|
|
1616
|
-
});
|
|
1617
|
-
throw new IntakeSuggestionError("LLM suggestion generation failed", { cause: error });
|
|
1618
|
-
}
|
|
1619
|
-
let proposals;
|
|
1620
|
-
try {
|
|
1621
|
-
proposals = toProposals(validateLlmSuggestionOutput(output));
|
|
1622
|
-
} catch (error) {
|
|
1623
|
-
this.metrics.increment("intake.suggestions.failed");
|
|
1624
|
-
throw error instanceof IntakeSuggestionError ? error : new IntakeSuggestionError("LLM suggestion output could not be validated", {
|
|
1625
|
-
cause: error
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
if (proposals.length === 0) {
|
|
1629
|
-
this.metrics.increment("intake.suggestions.failed");
|
|
1630
|
-
throw new IntakeSuggestionError("LLM suggestion output contained no usable proposals");
|
|
1631
|
-
}
|
|
1632
|
-
const hadQuestions = proposals.some((proposal2) => proposal2.kind === "question");
|
|
1633
|
-
let previousStatus = record.status;
|
|
1634
|
-
const updateOptions = {
|
|
1635
|
-
actorId: ctx.id,
|
|
1636
|
-
actorType: ctx.type,
|
|
1637
|
-
action: hadQuestions ? "information_requested" : "suggestions_added"
|
|
1638
|
-
};
|
|
1639
|
-
const updated = await this.store.update(id, updateOptions, (next) => {
|
|
1640
|
-
this.assertMutable(next, "generateSuggestions");
|
|
1641
|
-
previousStatus = next.status;
|
|
1642
|
-
if (next.status === "draft") {
|
|
1643
|
-
updateOptions.from = "draft";
|
|
1644
|
-
updateOptions.to = "collecting_information";
|
|
1645
|
-
next.status = "collecting_information";
|
|
1646
|
-
}
|
|
1647
|
-
next.llmSuggestions.push(...proposals);
|
|
1648
|
-
if (next.llmSuggestions.length > MAX_SUGGESTIONS) {
|
|
1649
|
-
next.llmSuggestions = next.llmSuggestions.slice(
|
|
1650
|
-
next.llmSuggestions.length - MAX_SUGGESTIONS
|
|
1651
|
-
);
|
|
1652
|
-
}
|
|
1653
|
-
});
|
|
1654
|
-
this.metrics.increment("intake.suggestions.succeeded");
|
|
1655
|
-
this.logger.info("intake", "intake.suggestions.succeeded", {
|
|
1656
|
-
intakeId: updated.id,
|
|
1657
|
-
projectId: updated.projectId,
|
|
1658
|
-
actorId: ctx.id,
|
|
1659
|
-
count: proposals.length
|
|
1660
|
-
});
|
|
1661
|
-
this.emit(hadQuestions ? "RequirementIntakeInformationRequested" : "RequirementIntakeUpdated", {
|
|
1662
|
-
intakeId: updated.id,
|
|
1663
|
-
projectId: updated.projectId,
|
|
1664
|
-
actorId: ctx.id,
|
|
1665
|
-
actorType: ctx.type,
|
|
1666
|
-
previousStatus,
|
|
1667
|
-
status: updated.status
|
|
1668
|
-
});
|
|
1669
|
-
return proposals;
|
|
1729
|
+
return generateSuggestions(this.intakeSuggestionsHost(), id, ctx, focus);
|
|
1670
1730
|
}
|
|
1671
1731
|
async acceptSuggestion(id, proposalId, ctx, expectedVersion) {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const proposal2 = this.findSuggestion(record, proposalId);
|
|
1675
|
-
if (proposal2.status !== "pending") {
|
|
1676
|
-
throw new IntakeValidationError([
|
|
1677
|
-
{ field: "suggestionId", message: `suggestion is already ${proposal2.status}` }
|
|
1678
|
-
]);
|
|
1679
|
-
}
|
|
1680
|
-
return this.store.update(
|
|
1732
|
+
return acceptSuggestion(
|
|
1733
|
+
this.intakeSuggestionsHost(),
|
|
1681
1734
|
id,
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
if (!target) {
|
|
1687
|
-
throw new IntakeValidationError([
|
|
1688
|
-
{ field: "suggestionId", message: `suggestion not found: ${proposalId}` }
|
|
1689
|
-
]);
|
|
1690
|
-
}
|
|
1691
|
-
this.applyProposal(next, target);
|
|
1692
|
-
target.status = "accepted";
|
|
1693
|
-
target.resolvedAt = Date.now();
|
|
1694
|
-
}
|
|
1695
|
-
).then((updated) => {
|
|
1696
|
-
this.afterMutation(updated, ctx, "RequirementIntakeUpdated");
|
|
1697
|
-
return updated;
|
|
1698
|
-
});
|
|
1735
|
+
proposalId,
|
|
1736
|
+
ctx,
|
|
1737
|
+
expectedVersion
|
|
1738
|
+
);
|
|
1699
1739
|
}
|
|
1700
1740
|
async rejectSuggestion(id, proposalId, ctx, expectedVersion) {
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
this.findSuggestion(record, proposalId);
|
|
1704
|
-
return this.store.update(
|
|
1741
|
+
return rejectSuggestion(
|
|
1742
|
+
this.intakeSuggestionsHost(),
|
|
1705
1743
|
id,
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
if (!target) {
|
|
1711
|
-
throw new IntakeValidationError([
|
|
1712
|
-
{ field: "suggestionId", message: `suggestion not found: ${proposalId}` }
|
|
1713
|
-
]);
|
|
1714
|
-
}
|
|
1715
|
-
if (target.status !== "pending") {
|
|
1716
|
-
throw new IntakeValidationError([
|
|
1717
|
-
{ field: "suggestionId", message: `suggestion is already ${target.status}` }
|
|
1718
|
-
]);
|
|
1719
|
-
}
|
|
1720
|
-
target.status = "rejected";
|
|
1721
|
-
target.resolvedAt = Date.now();
|
|
1722
|
-
}
|
|
1723
|
-
).then((updated) => {
|
|
1724
|
-
this.afterMutation(updated, ctx, "RequirementIntakeUpdated");
|
|
1725
|
-
return updated;
|
|
1726
|
-
});
|
|
1744
|
+
proposalId,
|
|
1745
|
+
ctx,
|
|
1746
|
+
expectedVersion
|
|
1747
|
+
);
|
|
1727
1748
|
}
|
|
1728
1749
|
// -------------------------------------------------------------------------
|
|
1729
1750
|
// Lifecycle
|
|
@@ -1947,6 +1968,22 @@ var RequirementIntakeService = class {
|
|
|
1947
1968
|
throw error;
|
|
1948
1969
|
}
|
|
1949
1970
|
}
|
|
1971
|
+
intakeSuggestionsHost() {
|
|
1972
|
+
void {
|
|
1973
|
+
requireRecord: this.requireRecord,
|
|
1974
|
+
assertMutable: this.assertMutable,
|
|
1975
|
+
generator: this.generator,
|
|
1976
|
+
metrics: this.metrics,
|
|
1977
|
+
logger: this.logger,
|
|
1978
|
+
store: this.store,
|
|
1979
|
+
emit: this.emit,
|
|
1980
|
+
findSuggestion: this.findSuggestion,
|
|
1981
|
+
updateMeta: this.updateMeta,
|
|
1982
|
+
applyProposal: this.applyProposal,
|
|
1983
|
+
afterMutation: this.afterMutation
|
|
1984
|
+
};
|
|
1985
|
+
return this;
|
|
1986
|
+
}
|
|
1950
1987
|
};
|
|
1951
1988
|
|
|
1952
1989
|
// src/types.ts
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IntakeOperation } from './authorization.js';
|
|
2
|
+
import type { IntakeLogger } from './logger.js';
|
|
3
|
+
import type { IntakeMetrics } from './metrics.js';
|
|
4
|
+
import type { RequirementIntakeStore, StoreUpdateOptions } from './store.js';
|
|
5
|
+
import { type LlmSuggestionGenerator } from './suggestions.js';
|
|
6
|
+
import type { IntakeContext, IntakeEvent, IntakeEventName, LlmSuggestionProposal, RequirementIntakeRecord } from './types.js';
|
|
7
|
+
export interface IntakeSuggestionsHost {
|
|
8
|
+
requireRecord(id: string, ctx: IntakeContext, operation: IntakeOperation): Promise<RequirementIntakeRecord>;
|
|
9
|
+
assertMutable(record: RequirementIntakeRecord, action: string): void;
|
|
10
|
+
generator: LlmSuggestionGenerator | undefined;
|
|
11
|
+
metrics: IntakeMetrics;
|
|
12
|
+
logger: IntakeLogger;
|
|
13
|
+
store: RequirementIntakeStore;
|
|
14
|
+
emit(event: IntakeEventName, data: Omit<IntakeEvent, 'event' | 'timestamp'>): void;
|
|
15
|
+
findSuggestion(record: RequirementIntakeRecord, proposalId: string): LlmSuggestionProposal;
|
|
16
|
+
updateMeta(ctx: IntakeContext, action: string, fields: string[], expectedVersion?: number | undefined): StoreUpdateOptions;
|
|
17
|
+
applyProposal(record: RequirementIntakeRecord, proposal: LlmSuggestionProposal): void;
|
|
18
|
+
afterMutation(record: RequirementIntakeRecord, ctx: IntakeContext, event: IntakeEventName): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function generateSuggestions(host: IntakeSuggestionsHost, id: string, ctx: IntakeContext, focus?: string[] | undefined): Promise<LlmSuggestionProposal[]>;
|
|
21
|
+
export declare function acceptSuggestion(host: IntakeSuggestionsHost, id: string, proposalId: string, ctx: IntakeContext, expectedVersion?: number | undefined): Promise<RequirementIntakeRecord>;
|
|
22
|
+
export declare function rejectSuggestion(host: IntakeSuggestionsHost, id: string, proposalId: string, ctx: IntakeContext, expectedVersion?: number | undefined): Promise<RequirementIntakeRecord>;
|
|
23
|
+
//# sourceMappingURL=service-suggestions.d.ts.map
|
package/dist/service.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { IntakeEventEmitter } from './events.js';
|
|
|
19
19
|
import { type IntakeLogger } from './logger.js';
|
|
20
20
|
import { type IntakeMetrics } from './metrics.js';
|
|
21
21
|
import type { RequirementIntakeStore } from './store.js';
|
|
22
|
-
import {
|
|
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;
|
|
@@ -85,5 +85,6 @@ export declare class RequirementIntakeService {
|
|
|
85
85
|
private afterMutation;
|
|
86
86
|
private emit;
|
|
87
87
|
private guardValidation;
|
|
88
|
+
private intakeSuggestionsHost;
|
|
88
89
|
}
|
|
89
90
|
//# sourceMappingURL=service.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/requirement-intake",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.35",
|
|
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.
|
|
30
|
+
"@wrongstack/core": "1.0.35",
|
|
31
31
|
"zod": "4.6.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|