@withtruing/companion 0.13.2 → 0.14.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/README.md +25 -26
- package/dist/anthropic-output-schema.js +41 -0
- package/dist/api-key-caller.js +9 -2
- package/dist/claude-cli-caller.js +107 -8
- package/dist/cli.js +837 -207
- package/dist/codex-binary.js +96 -0
- package/dist/codex-cli-caller.js +90 -16
- package/dist/config.js +177 -2
- package/dist/git-range.js +88 -46
- package/dist/http-client.js +95 -10
- package/dist/investigation-reader.js +12 -6
- package/dist/managed-repo.js +132 -0
- package/dist/observed-sources.js +132 -6
- package/dist/reader-result-contract.js +296 -2
- package/dist/repository-provider.js +608 -0
- package/dist/schema-entities.js +18 -18
- package/dist/service.js +7 -2
- package/dist/surface-mapper.js +53 -11
- package/dist/text-model.js +91 -8
- package/dist/whole-read/archive.js +196 -0
- package/dist/whole-read/checks.js +297 -0
- package/dist/whole-read/claims.js +196 -0
- package/dist/whole-read/coverage-ledger.js +51 -0
- package/dist/whole-read/decode.js +234 -0
- package/dist/whole-read/drafting.js +502 -0
- package/dist/whole-read/generated-rules.js +68 -0
- package/dist/whole-read/go-proof.js +255 -0
- package/dist/whole-read/grouping-prompt.js +19 -0
- package/dist/whole-read/grouping.js +247 -0
- package/dist/whole-read/identity.js +103 -0
- package/dist/whole-read/import-graph.js +293 -0
- package/dist/whole-read/inventory-collector.js +55 -0
- package/dist/whole-read/inventory-rules.js +141 -0
- package/dist/whole-read/lockfiles.js +947 -0
- package/dist/whole-read/notes.js +216 -0
- package/dist/whole-read/npm-proof.js +272 -0
- package/dist/whole-read/owner-decision-detectors.js +270 -0
- package/dist/whole-read/packing.js +612 -0
- package/dist/whole-read/pages.js +429 -0
- package/dist/whole-read/phase-policy.js +54 -0
- package/dist/whole-read/prd-build.js +279 -0
- package/dist/whole-read/prd-fidelity.js +14 -0
- package/dist/whole-read/python-proof.js +489 -0
- package/dist/whole-read/read-prompt.js +67 -0
- package/dist/whole-read/redaction.js +411 -0
- package/dist/whole-read/relationship-mapping.js +98 -0
- package/dist/whole-read/sanitize.js +266 -0
- package/dist/whole-read/secret-rules.js +26 -0
- package/dist/whole-read/structure.js +300 -0
- package/dist/whole-read/types.js +2 -0
- package/dist/whole-read-claims.js +12 -0
- package/dist/whole-read-git.js +340 -0
- package/dist/whole-read-pool.js +80 -0
- package/dist/whole-read-runner.js +945 -0
- package/dist/whole-read-shared-source.js +194 -0
- package/dist/whole-read-temp.js +479 -0
- package/package.json +8 -2
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { redactCanonicalText } from './redaction.js';
|
|
2
|
+
import { WHOLE_READ_PHASE_POLICY } from './phase-policy.js';
|
|
3
|
+
export const WHOLE_READ_PRD_CAPABILITY = 'whole-read-prd@1';
|
|
4
|
+
export const WHOLE_READ_PRD_UNIT_PROTOCOL = 'whole-read-prd-unit@2';
|
|
5
|
+
export const PRD_SOURCE_NOTICE = 'From the PRD — not yet checked against the code';
|
|
6
|
+
const citationSchema = {
|
|
7
|
+
type: 'object', additionalProperties: false,
|
|
8
|
+
required: ['sectionId', 'quote'],
|
|
9
|
+
properties: { sectionId: { type: 'string', minLength: 1 },
|
|
10
|
+
quote: { type: 'string', minLength: 1, maxLength: 300 } },
|
|
11
|
+
};
|
|
12
|
+
const laneSchema = {
|
|
13
|
+
type: 'object', additionalProperties: false, required: ['key', 'label', 'citation'],
|
|
14
|
+
properties: { key: { type: 'string', minLength: 1 }, label: { type: 'string', minLength: 1 },
|
|
15
|
+
citation: { ...citationSchema, type: ['object', 'null'] } },
|
|
16
|
+
};
|
|
17
|
+
const claimSchema = {
|
|
18
|
+
type: 'object', additionalProperties: false,
|
|
19
|
+
required: ['claimId', 'laneKey', 'text', 'citations', 'status', 'statusCitation', 'verdict'],
|
|
20
|
+
properties: { claimId: { type: 'string', minLength: 1 }, laneKey: { type: 'string', minLength: 1 },
|
|
21
|
+
text: { type: 'string' }, citations: { type: 'array', minItems: 1, items: citationSchema },
|
|
22
|
+
status: { type: 'string', enum: ['shipped', 'in_progress', 'planned', 'proposed', 'unset'] },
|
|
23
|
+
statusCitation: { ...citationSchema, type: ['object', 'null'] },
|
|
24
|
+
verdict: { type: 'string', enum: ['pending', 'supported', 'contradicted', 'not_established'] } },
|
|
25
|
+
};
|
|
26
|
+
/** One registered strict schema is passed directly by every PRD model adapter. */
|
|
27
|
+
export const prdUnitResultSchema = {
|
|
28
|
+
type: 'object', additionalProperties: false,
|
|
29
|
+
required: ['phase', 'lanes', 'claims', 'unmatchedSectionIds'],
|
|
30
|
+
properties: { phase: { type: 'string', enum: ['lanes', 'draft', 'check'] },
|
|
31
|
+
lanes: { type: 'array', items: laneSchema }, claims: { type: 'array', items: claimSchema },
|
|
32
|
+
unmatchedSectionIds: { type: 'array', items: { type: 'string', minLength: 1 } } },
|
|
33
|
+
};
|
|
34
|
+
const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
const exact = (value, keys) => record(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
36
|
+
const nonBlank = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
37
|
+
const integer = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
38
|
+
const citationKey = (value) => JSON.stringify([value.sectionId, value.startChar, value.endChar]);
|
|
39
|
+
function fragmentMap(fragment) {
|
|
40
|
+
const deliveredLength = Array.from(fragment.text).length;
|
|
41
|
+
if (!nonBlank(fragment.sectionId) || !integer(fragment.startChar) || !integer(fragment.endChar)
|
|
42
|
+
|| fragment.endChar <= fragment.startChar)
|
|
43
|
+
throw new TypeError('prd_citation_invalid');
|
|
44
|
+
if (fragment.sourceMap === undefined) {
|
|
45
|
+
if (fragment.endChar - fragment.startChar !== deliveredLength)
|
|
46
|
+
throw new TypeError('prd_citation_invalid');
|
|
47
|
+
return [{ kind: 'source', deliveredStart: 0, deliveredEnd: deliveredLength,
|
|
48
|
+
sourceStartChar: fragment.startChar, sourceEndChar: fragment.endChar }];
|
|
49
|
+
}
|
|
50
|
+
let delivered = 0;
|
|
51
|
+
let source = fragment.startChar;
|
|
52
|
+
for (const segment of fragment.sourceMap) {
|
|
53
|
+
if (!exact(segment, ['kind', 'deliveredStart', 'deliveredEnd', 'sourceStartChar', 'sourceEndChar'])
|
|
54
|
+
|| !['source', 'replacement'].includes(String(segment.kind))
|
|
55
|
+
|| !integer(segment.deliveredStart) || !integer(segment.deliveredEnd)
|
|
56
|
+
|| !integer(segment.sourceStartChar) || !integer(segment.sourceEndChar)
|
|
57
|
+
|| segment.deliveredStart !== delivered || segment.sourceStartChar !== source
|
|
58
|
+
|| segment.deliveredEnd <= segment.deliveredStart || segment.sourceEndChar <= segment.sourceStartChar
|
|
59
|
+
|| (segment.kind === 'source'
|
|
60
|
+
&& segment.deliveredEnd - segment.deliveredStart !== segment.sourceEndChar - segment.sourceStartChar)) {
|
|
61
|
+
throw new TypeError('prd_citation_invalid');
|
|
62
|
+
}
|
|
63
|
+
delivered = segment.deliveredEnd;
|
|
64
|
+
source = segment.sourceEndChar;
|
|
65
|
+
}
|
|
66
|
+
if (delivered !== deliveredLength || source !== fragment.endChar)
|
|
67
|
+
throw new TypeError('prd_citation_invalid');
|
|
68
|
+
return fragment.sourceMap;
|
|
69
|
+
}
|
|
70
|
+
function pointMatches(haystack, needle) {
|
|
71
|
+
const matches = [];
|
|
72
|
+
for (let start = 0; start + needle.length <= haystack.length; start += 1) {
|
|
73
|
+
if (needle.every((point, offset) => haystack[start + offset] === point)) {
|
|
74
|
+
matches.push({ start, end: start + needle.length });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return matches;
|
|
78
|
+
}
|
|
79
|
+
function normalized(value) {
|
|
80
|
+
const original = Array.from(value);
|
|
81
|
+
const points = [];
|
|
82
|
+
const spans = [];
|
|
83
|
+
for (let index = 0; index < original.length;) {
|
|
84
|
+
const start = index;
|
|
85
|
+
const whitespace = /\s/u.test(original[index]);
|
|
86
|
+
index += 1;
|
|
87
|
+
if (whitespace)
|
|
88
|
+
while (index < original.length && /\s/u.test(original[index]))
|
|
89
|
+
index += 1;
|
|
90
|
+
points.push(whitespace ? ' ' : original[start]);
|
|
91
|
+
spans.push({ start, end: index });
|
|
92
|
+
}
|
|
93
|
+
return { points, spans };
|
|
94
|
+
}
|
|
95
|
+
function canonicalSpan(fragment, deliveredStart, deliveredEnd) {
|
|
96
|
+
const relevant = fragmentMap(fragment).filter((segment) => segment.deliveredEnd > deliveredStart && segment.deliveredStart < deliveredEnd);
|
|
97
|
+
if (!relevant.length)
|
|
98
|
+
throw new TypeError('prd_citation_invalid');
|
|
99
|
+
const first = relevant[0];
|
|
100
|
+
const last = relevant.at(-1);
|
|
101
|
+
const startChar = first.kind === 'source'
|
|
102
|
+
? first.sourceStartChar + deliveredStart - first.deliveredStart : first.sourceStartChar;
|
|
103
|
+
const endChar = last.kind === 'source'
|
|
104
|
+
? last.sourceStartChar + deliveredEnd - last.deliveredStart : last.sourceEndChar;
|
|
105
|
+
if (startChar < fragment.startChar || endChar > fragment.endChar || endChar <= startChar) {
|
|
106
|
+
throw new TypeError('prd_citation_invalid');
|
|
107
|
+
}
|
|
108
|
+
return { sectionId: fragment.sectionId, startChar, endChar };
|
|
109
|
+
}
|
|
110
|
+
function citation(value, fragments) {
|
|
111
|
+
if (!exact(value, ['sectionId', 'quote']) || !nonBlank(value.sectionId) || !nonBlank(value.quote)
|
|
112
|
+
|| Array.from(value.quote).length > 300)
|
|
113
|
+
throw new TypeError('prd_citation_invalid');
|
|
114
|
+
const candidates = fragments.filter((fragment) => fragment.sectionId === value.sectionId);
|
|
115
|
+
if (!candidates.length)
|
|
116
|
+
throw new TypeError('prd_citation_invalid');
|
|
117
|
+
const quote = Array.from(value.quote);
|
|
118
|
+
const exactMatches = candidates.flatMap((fragment) => pointMatches(Array.from(fragment.text), quote).map((match) => ({ fragment, ...match })));
|
|
119
|
+
if (exactMatches.length > 1)
|
|
120
|
+
throw new TypeError('prd_citation_invalid');
|
|
121
|
+
if (exactMatches.length === 1)
|
|
122
|
+
return canonicalSpan(exactMatches[0].fragment, exactMatches[0].start, exactMatches[0].end);
|
|
123
|
+
const normalizedQuote = normalized(value.quote).points;
|
|
124
|
+
const normalizedMatches = candidates.flatMap((fragment) => {
|
|
125
|
+
const text = normalized(fragment.text);
|
|
126
|
+
return pointMatches(text.points, normalizedQuote).map((match) => ({ fragment,
|
|
127
|
+
start: text.spans[match.start].start, end: text.spans[match.end - 1].end }));
|
|
128
|
+
});
|
|
129
|
+
if (normalizedMatches.length !== 1)
|
|
130
|
+
throw new TypeError('prd_citation_invalid');
|
|
131
|
+
return canonicalSpan(normalizedMatches[0].fragment, normalizedMatches[0].start, normalizedMatches[0].end);
|
|
132
|
+
}
|
|
133
|
+
function excerpt(value, fragments) {
|
|
134
|
+
const fragment = fragments.find((item) => item.sectionId === value.sectionId
|
|
135
|
+
&& value.startChar >= item.startChar && value.endChar <= item.endChar);
|
|
136
|
+
if (!fragment)
|
|
137
|
+
throw new TypeError('prd_citation_invalid');
|
|
138
|
+
const relevant = fragmentMap(fragment).filter((segment) => segment.sourceEndChar > value.startChar && segment.sourceStartChar < value.endChar);
|
|
139
|
+
if (!relevant.length)
|
|
140
|
+
throw new TypeError('prd_citation_invalid');
|
|
141
|
+
const first = relevant[0];
|
|
142
|
+
const last = relevant.at(-1);
|
|
143
|
+
const start = first.kind === 'source'
|
|
144
|
+
? first.deliveredStart + value.startChar - first.sourceStartChar : first.deliveredStart;
|
|
145
|
+
const end = last.kind === 'source'
|
|
146
|
+
? last.deliveredStart + value.endChar - last.sourceStartChar : last.deliveredEnd;
|
|
147
|
+
return Array.from(fragment.text).slice(start, end).join('');
|
|
148
|
+
}
|
|
149
|
+
function explicitDivision(value, fragments) {
|
|
150
|
+
return /\b(audience|persona|product|customers?|users?|admins?|merchants?|operators?|teams?)\b/i.test(excerpt(value, fragments));
|
|
151
|
+
}
|
|
152
|
+
function explicitStatus(status, value, fragments) {
|
|
153
|
+
const text = excerpt(value, fragments);
|
|
154
|
+
const patterns = {
|
|
155
|
+
shipped: /\b(shipped|released|live|already built)\b/i,
|
|
156
|
+
in_progress: /\b(in progress|being built|under development)\b/i,
|
|
157
|
+
planned: /\b(planned|not yet built|scheduled|for q[1-4])\b/i,
|
|
158
|
+
proposed: /\b(proposed|proposal|candidate)\b/i,
|
|
159
|
+
};
|
|
160
|
+
return status !== 'unset' && patterns[status].test(text);
|
|
161
|
+
}
|
|
162
|
+
function parsedClaim(value, unit) {
|
|
163
|
+
if (!exact(value, ['claimId', 'laneKey', 'text', 'citations', 'status', 'statusCitation', 'verdict'])
|
|
164
|
+
|| !nonBlank(value.claimId) || !nonBlank(value.laneKey) || typeof value.text !== 'string'
|
|
165
|
+
|| !Array.isArray(value.citations) || value.citations.length === 0
|
|
166
|
+
|| !['shipped', 'in_progress', 'planned', 'proposed', 'unset'].includes(String(value.status))
|
|
167
|
+
|| !['pending', 'supported', 'contradicted', 'not_established'].includes(String(value.verdict))) {
|
|
168
|
+
throw new TypeError('prd_citation_invalid');
|
|
169
|
+
}
|
|
170
|
+
const citations = value.citations.map((item) => citation(item, unit.fragments));
|
|
171
|
+
const status = value.status;
|
|
172
|
+
const statusCitation = value.statusCitation === null ? null : citation(value.statusCitation, unit.fragments);
|
|
173
|
+
if ((status === 'unset') !== (statusCitation === null)
|
|
174
|
+
|| (statusCitation && (!citations.some((item) => citationKey(item) === citationKey(statusCitation))
|
|
175
|
+
|| !explicitStatus(status, statusCitation, unit.fragments))))
|
|
176
|
+
throw new TypeError('prd_citation_invalid');
|
|
177
|
+
return { claimId: value.claimId, laneKey: value.laneKey, text: value.text,
|
|
178
|
+
citations, status, statusCitation, verdict: value.verdict };
|
|
179
|
+
}
|
|
180
|
+
export function validatePrdUnitResult(unit, value) {
|
|
181
|
+
if (!exact(value, ['phase', 'lanes', 'claims', 'unmatchedSectionIds']) || value.phase !== unit.phase
|
|
182
|
+
|| !Array.isArray(value.lanes) || !Array.isArray(value.claims) || !Array.isArray(value.unmatchedSectionIds)
|
|
183
|
+
|| value.unmatchedSectionIds.some((id) => !nonBlank(id)))
|
|
184
|
+
throw new TypeError('prd_citation_invalid');
|
|
185
|
+
let lanes = [];
|
|
186
|
+
if (unit.phase === 'lanes') {
|
|
187
|
+
if (value.claims.length || value.unmatchedSectionIds.length)
|
|
188
|
+
throw new TypeError('prd_citation_invalid');
|
|
189
|
+
lanes = value.lanes.map((candidate) => {
|
|
190
|
+
if (!exact(candidate, ['key', 'label', 'citation']) || !nonBlank(candidate.key)
|
|
191
|
+
|| !nonBlank(candidate.label) || candidate.citation === null)
|
|
192
|
+
throw new TypeError('prd_citation_invalid');
|
|
193
|
+
const cited = citation(candidate.citation, unit.fragments);
|
|
194
|
+
if (!explicitDivision(cited, unit.fragments))
|
|
195
|
+
throw new TypeError('prd_citation_invalid');
|
|
196
|
+
return { key: candidate.key, label: candidate.label, citation: cited };
|
|
197
|
+
});
|
|
198
|
+
if (!lanes.length)
|
|
199
|
+
lanes = [{ key: 'document', label: 'Document', citation: null }];
|
|
200
|
+
if (new Set(lanes.map((lane) => lane.key)).size !== lanes.length)
|
|
201
|
+
throw new TypeError('prd_citation_invalid');
|
|
202
|
+
return { phase: unit.phase, lanes, claims: [], unmatchedSectionIds: [] };
|
|
203
|
+
}
|
|
204
|
+
if (value.lanes.length)
|
|
205
|
+
throw new TypeError('prd_citation_invalid');
|
|
206
|
+
const claims = value.claims.map((candidate) => parsedClaim(candidate, unit));
|
|
207
|
+
const unmatched = [...new Set(value.unmatchedSectionIds)];
|
|
208
|
+
const expected = new Set(unit.fragments.map((fragment) => fragment.sectionId));
|
|
209
|
+
const accounted = new Set([...claims.flatMap((claim) => claim.citations.map((item) => item.sectionId)), ...unmatched]);
|
|
210
|
+
if (expected.size !== accounted.size || [...expected].some((id) => !accounted.has(id))
|
|
211
|
+
|| [...accounted].some((id) => !expected.has(id)))
|
|
212
|
+
throw new TypeError('prd_citation_invalid');
|
|
213
|
+
if (unit.phase === 'draft') {
|
|
214
|
+
if (claims.some((claim) => claim.verdict !== 'pending'
|
|
215
|
+
|| !unit.lanes.some((lane) => lane.key === claim.laneKey)))
|
|
216
|
+
throw new TypeError('prd_citation_invalid');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const candidates = new Map(unit.claims.map((claim) => [claim.claimId, claim]));
|
|
220
|
+
if (claims.length !== candidates.size || claims.some((claim) => {
|
|
221
|
+
const prior = candidates.get(claim.claimId);
|
|
222
|
+
return !prior || claim.verdict === 'pending' || claim.text !== prior.text || claim.laneKey !== prior.laneKey
|
|
223
|
+
|| claim.citations.length !== prior.citations.length
|
|
224
|
+
|| claim.citations.some((item, index) => citationKey(item) !== citationKey(prior.citations[index]));
|
|
225
|
+
}))
|
|
226
|
+
throw new TypeError('prd_citation_invalid');
|
|
227
|
+
}
|
|
228
|
+
return { phase: unit.phase, lanes: [], claims, unmatchedSectionIds: unmatched.sort() };
|
|
229
|
+
}
|
|
230
|
+
export function createPrdPrompt(unit) {
|
|
231
|
+
if (unit.protocolVersion !== WHOLE_READ_PRD_UNIT_PROTOCOL || unit.source.kind !== 'prd'
|
|
232
|
+
|| !/^sha256:[0-9a-f]{64}$/.test(unit.source.textSha256)
|
|
233
|
+
|| !Number.isSafeInteger(unit.outputTokens) || unit.outputTokens < 1)
|
|
234
|
+
throw new TypeError('prd_source_mismatch');
|
|
235
|
+
unit.fragments.forEach(fragmentMap);
|
|
236
|
+
const instruction = unit.phase === 'lanes'
|
|
237
|
+
? 'Return only explicit product or audience divisions with citations; return no lanes when none are stated.'
|
|
238
|
+
: unit.phase === 'draft'
|
|
239
|
+
? 'Draft faithful intent claims. Account for every section with a cited claim or unmatchedSectionIds. Do not infer status.'
|
|
240
|
+
: 'Check every candidate only against its cited PRD text. Preserve citations and abstain when unsupported.';
|
|
241
|
+
const cite = (item) => ({ sectionId: item.sectionId, quote: excerpt(item, unit.fragments) });
|
|
242
|
+
return `${instruction}\nCite by section id plus an exact short quote copied from that section's text. `
|
|
243
|
+
+ `Use 1 to 300 characters and enough text to be unique within the section.\n`
|
|
244
|
+
+ `Schema:${JSON.stringify(prdUnitResultSchema)}\nInput:${JSON.stringify({
|
|
245
|
+
phase: unit.phase, source: unit.source,
|
|
246
|
+
fragments: unit.fragments.map((fragment) => ({ sectionId: fragment.sectionId, text: fragment.text })),
|
|
247
|
+
lanes: unit.lanes.map((lane) => ({ key: lane.key, label: lane.label })),
|
|
248
|
+
claims: unit.claims.map((claim) => ({ ...claim, citations: claim.citations.map(cite),
|
|
249
|
+
statusCitation: claim.statusCitation && cite(claim.statusCitation) })),
|
|
250
|
+
unmatchedSectionIds: unit.unmatchedSectionIds,
|
|
251
|
+
})}`;
|
|
252
|
+
}
|
|
253
|
+
export function redactPrdFragment(fragment) {
|
|
254
|
+
const sourcePoints = Array.from(fragment.text);
|
|
255
|
+
if (fragment.endChar - fragment.startChar !== sourcePoints.length)
|
|
256
|
+
throw new TypeError('prd_citation_invalid');
|
|
257
|
+
const redacted = redactCanonicalText({ text: fragment.text });
|
|
258
|
+
const byteOffsets = [0];
|
|
259
|
+
for (const point of sourcePoints)
|
|
260
|
+
byteOffsets.push(byteOffsets.at(-1) + Buffer.byteLength(point));
|
|
261
|
+
const characterByByte = new Map(byteOffsets.map((byte, index) => [byte, fragment.startChar + index]));
|
|
262
|
+
const characterAtByte = (byte) => {
|
|
263
|
+
const index = characterByByte.get(byte);
|
|
264
|
+
if (index === undefined)
|
|
265
|
+
throw new TypeError('prd_citation_invalid');
|
|
266
|
+
return index;
|
|
267
|
+
};
|
|
268
|
+
return { ...fragment, text: redacted.text, sourceMap: redacted.mapping.segments.map((segment) => ({
|
|
269
|
+
kind: segment.kind, deliveredStart: segment.deliveredStart, deliveredEnd: segment.deliveredEnd,
|
|
270
|
+
sourceStartChar: characterAtByte(segment.canonicalStartByte),
|
|
271
|
+
sourceEndChar: characterAtByte(segment.canonicalEndByte),
|
|
272
|
+
})) };
|
|
273
|
+
}
|
|
274
|
+
export async function runPrdUnit(unit, model, signal, backend) {
|
|
275
|
+
const response = await backend({ prompt: createPrdPrompt(unit), schema: prdUnitResultSchema,
|
|
276
|
+
model, phase: `prd_${unit.phase}`,
|
|
277
|
+
maxTokens: WHOLE_READ_PHASE_POLICY[`prd_${unit.phase}`].maxOutputTokens, signal });
|
|
278
|
+
return { ...response, result: validatePrdUnitResult(unit, response.result) };
|
|
279
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const PRD_FIDELITY_POLICY_VERSION = 'prd-fidelity@1';
|
|
2
|
+
export const PRD_NOT_CHECKED_LABEL = 'From the PRD — not yet checked against the code';
|
|
3
|
+
const sourceMetadata = () => ({ codeChecked: false, sourceNotice: PRD_NOT_CHECKED_LABEL });
|
|
4
|
+
/** Status is data from the checked passage, never a phase/list-order heuristic. */
|
|
5
|
+
export function prdBoardMetadata(input = {}) {
|
|
6
|
+
const checked = input.checkedStatusPassage ?? null;
|
|
7
|
+
return {
|
|
8
|
+
status: checked?.status ?? 'unset',
|
|
9
|
+
statusEvidenceId: checked?.evidenceId ?? null,
|
|
10
|
+
statusCitation: checked?.citedPrdRange ?? null,
|
|
11
|
+
card: sourceMetadata(),
|
|
12
|
+
lane: sourceMetadata(),
|
|
13
|
+
};
|
|
14
|
+
}
|