@innofeight/global-workflow 0.0.1 → 0.0.2

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.
@@ -0,0 +1,398 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ export const MIRROR_EVIDENCE_VERSION = 1
4
+ export const ACCEPTANCE_KINDS = ['TASK_AC', 'HUMAN_QA_AC']
5
+ export const EVIDENCE_STRENGTHS = ['CONNECTOR-OBSERVED', 'HUMAN-ATTESTED']
6
+
7
+ const contracts = {
8
+ TASK_AC: {
9
+ heading: 'Task Acceptance Criteria',
10
+ idPattern: /^TAC-\d{2}\b/u,
11
+ acceptedLabels: ['Task Acceptance Criteria', 'AC']
12
+ },
13
+ HUMAN_QA_AC: {
14
+ heading: 'Human QA Acceptance Criteria',
15
+ applicabilityHeading: 'Human QA Applicability',
16
+ idPattern: /^HQA-\d{2}\b/u,
17
+ acceptedLabels: ['Human QA Acceptance Criteria']
18
+ }
19
+ }
20
+
21
+ function contract(kind) {
22
+ const selected = contracts[kind]
23
+ if (!selected) throw new Error(`UNSUPPORTED ACCEPTANCE KIND: ${kind}`)
24
+ return selected
25
+ }
26
+
27
+ function normalizeLines(markdown) {
28
+ return String(markdown)
29
+ .replace(/\r\n?/gu, '\n')
30
+ .split('\n')
31
+ .map((line) => line.replace(/[ \t]+$/gu, ''))
32
+ }
33
+
34
+ function sectionLines(markdown, heading) {
35
+ const lines = normalizeLines(markdown)
36
+ const matchingHeadings = lines.flatMap((line, index) => {
37
+ const match = line.match(/^(#{1,6})\s+(.+?)\s*$/u)
38
+ return match && match[2] === heading
39
+ ? [{ index, level: match[1].length }]
40
+ : []
41
+ })
42
+ const headings = matchingHeadings.filter(({ level }) => level === 2)
43
+ if (headings.length !== 1)
44
+ return {
45
+ diagnostics: [
46
+ headings.length === 0 && matchingHeadings.length > 0
47
+ ? `WRONG-LEVEL CANONICAL HEADING: ${heading}`
48
+ : headings.length === 0
49
+ ? `MISSING CANONICAL HEADING: ${heading}`
50
+ : `AMBIGUOUS CANONICAL HEADING: ${heading}`
51
+ ],
52
+ lines: []
53
+ }
54
+
55
+ const [{ index }] = headings
56
+ let end = lines.length
57
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
58
+ const match = lines[cursor].match(/^(#{1,6})\s+/u)
59
+ if (match && match[1].length <= 2) {
60
+ end = cursor
61
+ break
62
+ }
63
+ }
64
+ return { diagnostics: [], lines: lines.slice(index + 1, end) }
65
+ }
66
+
67
+ function paragraphs(lines) {
68
+ return lines
69
+ .join('\n')
70
+ .split(/\n[ \t]*\n/gu)
71
+ .map((paragraph) => paragraph.replace(/^\n+|\n+$/gu, ''))
72
+ .filter(Boolean)
73
+ }
74
+
75
+ function sha256(value) {
76
+ return createHash('sha256').update(value, 'utf8').digest('hex')
77
+ }
78
+
79
+ export function canonicalAcceptance(markdown, kind) {
80
+ const selected = contract(kind)
81
+ if (kind === 'HUMAN_QA_AC') {
82
+ const applicability = sectionLines(markdown, selected.applicabilityHeading)
83
+ if (applicability.diagnostics.length > 0)
84
+ return {
85
+ kind,
86
+ applicability: 'REQUIRED',
87
+ criteria: [],
88
+ ids: [],
89
+ normalized: '',
90
+ fingerprint: null,
91
+ diagnostics: applicability.diagnostics,
92
+ valid: false
93
+ }
94
+ if (applicability.diagnostics.length === 0) {
95
+ const applicabilityParagraphs = paragraphs(applicability.lines)
96
+ const none = applicabilityParagraphs.some((paragraph) =>
97
+ /^(none|not applicable)\b/iu.test(paragraph)
98
+ )
99
+ const required = applicabilityParagraphs.some((paragraph) =>
100
+ /^required\b/iu.test(paragraph)
101
+ )
102
+ if (none && required)
103
+ return {
104
+ kind,
105
+ applicability: 'REQUIRED',
106
+ criteria: [],
107
+ ids: [],
108
+ normalized: '',
109
+ fingerprint: null,
110
+ diagnostics: ['CONFLICTING HUMAN QA APPLICABILITY'],
111
+ valid: false
112
+ }
113
+ if (none) {
114
+ const criteriaSection = sectionLines(markdown, selected.heading)
115
+ if (criteriaSection.diagnostics.length === 0)
116
+ return {
117
+ kind,
118
+ applicability: 'REQUIRED',
119
+ criteria: [],
120
+ ids: [],
121
+ normalized: '',
122
+ fingerprint: null,
123
+ diagnostics: ['CONFLICTING HUMAN QA APPLICABILITY'],
124
+ valid: false
125
+ }
126
+ return {
127
+ kind,
128
+ applicability: 'NONE',
129
+ criteria: [],
130
+ ids: [],
131
+ normalized: '',
132
+ fingerprint: null,
133
+ diagnostics: [],
134
+ valid: true
135
+ }
136
+ }
137
+ if (required) {
138
+ // Continue into the canonical HQA criteria section below.
139
+ } else
140
+ return {
141
+ kind,
142
+ applicability: 'REQUIRED',
143
+ criteria: [],
144
+ ids: [],
145
+ normalized: '',
146
+ fingerprint: null,
147
+ diagnostics: ['INVALID HUMAN QA APPLICABILITY'],
148
+ valid: false
149
+ }
150
+ }
151
+ }
152
+ const section = sectionLines(markdown, selected.heading)
153
+ if (section.diagnostics.length > 0)
154
+ return {
155
+ kind,
156
+ applicability: 'REQUIRED',
157
+ criteria: [],
158
+ ids: [],
159
+ normalized: '',
160
+ fingerprint: null,
161
+ diagnostics: section.diagnostics,
162
+ valid: false
163
+ }
164
+ const sectionParagraphs = paragraphs(section.lines)
165
+ const criteria = sectionParagraphs.filter((paragraph) =>
166
+ selected.idPattern.test(paragraph)
167
+ )
168
+ const ids = criteria.map(
169
+ (criterion) => criterion.match(selected.idPattern)?.[0]
170
+ )
171
+ const diagnostics = [...section.diagnostics]
172
+
173
+ if (criteria.length === 0) diagnostics.push(`NO CANONICAL ${kind} CRITERIA`)
174
+ if (criteria.some((criterion) => criterion.includes('[truncated]')))
175
+ diagnostics.push(`TRUNCATED ${kind} CONTRACT`)
176
+ if (new Set(ids).size !== ids.length)
177
+ diagnostics.push(`DUPLICATE ${kind} IDS`)
178
+ if (
179
+ sectionParagraphs.some((paragraph) => {
180
+ const prefix = kind === 'TASK_AC' ? 'TAC-' : 'HQA-'
181
+ return paragraph.startsWith(prefix) && !selected.idPattern.test(paragraph)
182
+ })
183
+ )
184
+ diagnostics.push(`MALFORMED ${kind} ID`)
185
+
186
+ const normalized = criteria.join('\n\n')
187
+ return {
188
+ kind,
189
+ applicability: 'REQUIRED',
190
+ criteria,
191
+ ids,
192
+ normalized,
193
+ fingerprint: normalized ? `sha256:${sha256(normalized)}` : null,
194
+ diagnostics,
195
+ valid: diagnostics.length === 0
196
+ }
197
+ }
198
+
199
+ function field(block, name) {
200
+ return block.match(new RegExp(`(?:^|\\n)${name}:\\s*(.+)$`, 'mu'))?.[1].trim()
201
+ }
202
+
203
+ export function parseMirrorEvidence(text) {
204
+ const normalized = String(text)
205
+ .replace(/\r\n?/gu, '\n')
206
+ .replaceAll(' | ', '\n')
207
+ const starts = [
208
+ ...normalized.matchAll(/NATIVE ACCEPTANCE MIRROR EVIDENCE v(\d+)/gu)
209
+ ]
210
+ return starts.map((match, index) => {
211
+ const block = normalized.slice(
212
+ match.index,
213
+ starts[index + 1]?.index ?? normalized.length
214
+ )
215
+ const ids = field(block, 'Acceptance IDs')
216
+ ?.split(',')
217
+ .map((id) => id.trim())
218
+ return {
219
+ version: Number(match[1]),
220
+ taskId: field(block, 'ClickUp task ID') ?? null,
221
+ taskTitle: field(block, 'Task title') ?? null,
222
+ kind: field(block, 'Acceptance kind'),
223
+ fingerprint: field(block, 'Canonical fingerprint'),
224
+ ids: ids ?? [],
225
+ label: field(block, 'Native checklist label'),
226
+ strength: field(block, 'Evidence strength'),
227
+ mirrorMatched: field(block, 'Mirror matched canonical criteria'),
228
+ initialUnchecked: field(block, 'Initial unchecked state confirmed'),
229
+ recordedAt: field(block, 'Recorded at') ?? null
230
+ }
231
+ })
232
+ }
233
+
234
+ function sameValues(left, right) {
235
+ return (
236
+ left.length === right.length &&
237
+ left.every((value, index) => value === right[index])
238
+ )
239
+ }
240
+
241
+ export function validateMirrorEvidence(
242
+ evidence,
243
+ canonical,
244
+ { taskId, taskTitle } = {}
245
+ ) {
246
+ const selected = contract(canonical.kind)
247
+ const diagnostics = []
248
+ if (evidence.version !== MIRROR_EVIDENCE_VERSION)
249
+ diagnostics.push('UNSUPPORTED EVIDENCE VERSION')
250
+ if (evidence.kind !== canonical.kind)
251
+ diagnostics.push('ACCEPTANCE KIND MISMATCH')
252
+ if (evidence.fingerprint !== canonical.fingerprint)
253
+ diagnostics.push('CANONICAL FINGERPRINT MISMATCH')
254
+ if (!sameValues(evidence.ids, canonical.ids))
255
+ diagnostics.push('ACCEPTANCE IDS MISMATCH')
256
+ if (!selected.acceptedLabels.includes(evidence.label))
257
+ diagnostics.push('CHECKLIST LABEL NOT ACCEPTED')
258
+ if (!EVIDENCE_STRENGTHS.includes(evidence.strength))
259
+ diagnostics.push('INVALID EVIDENCE STRENGTH')
260
+ if (evidence.mirrorMatched !== 'YES')
261
+ diagnostics.push('MIRROR MATCH NOT CONFIRMED')
262
+ if (evidence.initialUnchecked !== 'YES')
263
+ diagnostics.push('INITIAL UNCHECKED STATE NOT CONFIRMED')
264
+ if (taskId && evidence.taskId !== taskId)
265
+ diagnostics.push('CLICKUP TASK ID MISMATCH')
266
+ if (taskTitle && evidence.taskTitle !== taskTitle)
267
+ diagnostics.push('TASK TITLE MISMATCH')
268
+ return { valid: canonical.valid && diagnostics.length === 0, diagnostics }
269
+ }
270
+
271
+ function reusableEvidence(records, canonical, binding) {
272
+ return (
273
+ records.find(
274
+ (record) => validateMirrorEvidence(record, canonical, binding).valid
275
+ ) ?? null
276
+ )
277
+ }
278
+
279
+ export function evaluateAcceptanceMirrors({
280
+ markdown,
281
+ checklistsCount,
282
+ evidenceText = '',
283
+ taskId,
284
+ taskTitle
285
+ }) {
286
+ const task = canonicalAcceptance(markdown, 'TASK_AC')
287
+ const humanQa = canonicalAcceptance(markdown, 'HUMAN_QA_AC')
288
+ const evidence = parseMirrorEvidence(evidenceText)
289
+ const canonicalDiagnostics = [...task.diagnostics, ...humanQa.diagnostics]
290
+ if (canonicalDiagnostics.length > 0)
291
+ return {
292
+ pass: false,
293
+ blocked: true,
294
+ closeAgentSegment: true,
295
+ diagnostics: canonicalDiagnostics,
296
+ task,
297
+ humanQa,
298
+ reusable: [],
299
+ repair: [],
300
+ repairPrompt: null
301
+ }
302
+
303
+ const required = humanQa.applicability === 'REQUIRED' ? [humanQa] : []
304
+ const reusable = required.flatMap((canonical) => {
305
+ const matching = reusableEvidence(evidence, canonical, {
306
+ taskId,
307
+ taskTitle
308
+ })
309
+ return matching ? [{ kind: canonical.kind, evidence: matching }] : []
310
+ })
311
+ const repair = required
312
+ .filter(
313
+ (canonical) => !reusable.some(({ kind }) => kind === canonical.kind)
314
+ )
315
+ .map(({ kind }) => kind)
316
+ if (Number(checklistsCount) < required.length && repair.length === 0)
317
+ for (const { kind } of required)
318
+ if (!repair.includes(kind)) repair.push(kind)
319
+
320
+ return {
321
+ pass: repair.length === 0,
322
+ blocked: false,
323
+ closeAgentSegment: repair.length > 0,
324
+ diagnostics: [],
325
+ task,
326
+ humanQa,
327
+ reusable,
328
+ repair,
329
+ repairPrompt: repair.includes('HUMAN_QA_AC')
330
+ ? formatHqaChecklistRepair(humanQa)
331
+ : null
332
+ }
333
+ }
334
+
335
+ export function formatHqaChecklistRepair(humanQa) {
336
+ if (
337
+ humanQa.kind !== 'HUMAN_QA_AC' ||
338
+ humanQa.applicability !== 'REQUIRED' ||
339
+ !humanQa.valid
340
+ )
341
+ throw new Error('VALID REQUIRED HUMAN QA ACCEPTANCE CRITERIA REQUIRED')
342
+ return [
343
+ 'HQA CHECKLIST REPAIR REQUIRED',
344
+ '',
345
+ 'Checklist name:',
346
+ 'Human QA Acceptance Criteria',
347
+ '',
348
+ '```text',
349
+ ...humanQa.criteria,
350
+ '```',
351
+ '',
352
+ 'Then reply exactly:',
353
+ '',
354
+ '`checklist added`'
355
+ ].join('\n')
356
+ }
357
+
358
+ export function formatMirrorEvidence({
359
+ canonical,
360
+ label,
361
+ strength,
362
+ recordedAt,
363
+ taskId,
364
+ taskTitle
365
+ }) {
366
+ const evidence = {
367
+ version: MIRROR_EVIDENCE_VERSION,
368
+ kind: canonical.kind,
369
+ fingerprint: canonical.fingerprint,
370
+ ids: canonical.ids,
371
+ label,
372
+ strength,
373
+ mirrorMatched: 'YES',
374
+ initialUnchecked: 'YES',
375
+ recordedAt: recordedAt ?? null,
376
+ taskId: taskId ?? null,
377
+ taskTitle: taskTitle ?? null
378
+ }
379
+ const result = validateMirrorEvidence(evidence, canonical)
380
+ if (!result.valid) throw new Error(result.diagnostics.join('; '))
381
+ return [
382
+ `NATIVE ACCEPTANCE MIRROR EVIDENCE v${MIRROR_EVIDENCE_VERSION}`,
383
+ ...(taskId ? [`ClickUp task ID: ${taskId}`] : []),
384
+ ...(taskTitle ? [`Task title: ${taskTitle}`] : []),
385
+ `Acceptance kind: ${canonical.kind}`,
386
+ `Canonical fingerprint: ${canonical.fingerprint}`,
387
+ `Acceptance IDs: ${canonical.ids.join(',')}`,
388
+ `Native checklist label: ${label}`,
389
+ `Evidence strength: ${strength}`,
390
+ 'Mirror matched canonical criteria: YES',
391
+ 'Initial unchecked state confirmed: YES',
392
+ ...(recordedAt ? [`Recorded at: ${recordedAt}`] : [])
393
+ ].join('\n')
394
+ }
395
+
396
+ export function formatMirrorEvidenceLedgerPage(options) {
397
+ return `\`\`\`text\n${formatMirrorEvidence(options)}\n\`\`\``
398
+ }