@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.
- package/README.md +32 -5
- package/package.json +12 -4
- package/provenance.json +294 -0
- package/rules/SHARED_WORKFLOW_RULES.md +653 -0
- package/scripts/lib/acceptance-mirror.mjs +398 -0
- package/scripts/lib/aet-v2.mjs +673 -0
- package/scripts/lib/clickup-task-comments.mjs +65 -0
- package/scripts/lib/post-merge-evidence.mjs +114 -0
- package/scripts/lib/task-branch.mjs +370 -0
- package/hello-world.mjs +0 -1
|
@@ -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
|
+
}
|