@gitroll/tacitt-cli 0.0.0-stage → 0.1.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 +84 -2
- package/bin/INDEX.md +9 -0
- package/bin/tacitt.cjs +16 -0
- package/dist/agent-setup-B85GaVIz.mjs +71 -0
- package/dist/agent-setup-worker.mjs +56 -0
- package/dist/cli.mjs +1343 -0
- package/dist/codex-title-worker.mjs +215 -0
- package/dist/configured-preview-worker.mjs +133 -0
- package/dist/configured-session-worker.mjs +117 -0
- package/dist/frame-D1j9R8xX.mjs +84 -0
- package/dist/heap-1FYhGds4.mjs +75 -0
- package/dist/heap-tfU61Ehy.mjs +7253 -0
- package/dist/job--TceKk0l.mjs +194 -0
- package/dist/locate-Bn32k2se.mjs +10647 -0
- package/dist/model-7Nu7szNP.mjs +41 -0
- package/dist/operation-executor.mjs +181 -0
- package/dist/preview-worker.mjs +22 -0
- package/dist/privacy-validator.mjs +46 -0
- package/dist/query-Da-KDbSV.mjs +14511 -0
- package/dist/run-CCATsHS2.mjs +916 -0
- package/dist/session-process.mjs +90 -0
- package/dist/src-BngGpFKd.mjs +16416 -0
- package/dist/src-CPPjOyHZ.mjs +12782 -0
- package/dist/src-DynBRDBc.mjs +6590 -0
- package/dist/store-database-worker.mjs +14 -0
- package/dist/web/THIRD_PARTY_LICENSES.md +388 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-D_uavGvn.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-RFb1v-CK.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-BmRLXWpJ.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-DCQwxkRk.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-700-normal-CkZN0sRy.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-700-normal-wyJ6jzdA.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-DZH6Xh-4.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-aMgXseD4.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-BV5B8Are.woff +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-CkEo6LDy.woff2 +0 -0
- package/dist/web/assets/index-CWQ05PFl.js +67 -0
- package/dist/web/assets/index-J-AWt_cW.css +1 -0
- package/dist/web/index.html +14 -0
- package/package.json +56 -3
- package/skills/INDEX.md +9 -0
- package/skills/tacitt/INDEX.md +11 -0
- package/skills/tacitt/SKILL.md +42 -0
- package/skills/tacitt/references/INDEX.md +15 -0
- package/skills/tacitt/references/b-method.md +22 -0
- package/skills/tacitt/references/bootstrap.md +109 -0
- package/skills/tacitt/references/classification.md +83 -0
- package/skills/tacitt/references/commands.md +147 -0
- package/skills/tacitt/references/journey.md +52 -0
- package/skills/tacitt/references/parallel-review.md +64 -0
- package/skills/tacitt/references/send.md +30 -0
- package/skills/tacitt/scripts/INDEX.md +43 -0
- package/skills/tacitt/scripts/answer.mjs +257 -0
- package/skills/tacitt/scripts/describe.mjs +55 -0
- package/skills/tacitt/scripts/journey-card-consolidation.d.mts +8 -0
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +146 -0
- package/skills/tacitt/scripts/journey-classification-metadata.d.mts +16 -0
- package/skills/tacitt/scripts/journey-classification-metadata.mjs +111 -0
- package/skills/tacitt/scripts/journey-classification-progress.mjs +194 -0
- package/skills/tacitt/scripts/journey-classification-tasks.d.mts +18 -0
- package/skills/tacitt/scripts/journey-classification-tasks.mjs +345 -0
- package/skills/tacitt/scripts/journey-classification-validation.d.mts +20 -0
- package/skills/tacitt/scripts/journey-classification-validation.mjs +219 -0
- package/skills/tacitt/scripts/journey-classification.mjs +145 -0
- package/skills/tacitt/scripts/journey-consolidation-submit.mjs +190 -0
- package/skills/tacitt/scripts/journey-consolidation.d.mts +9 -0
- package/skills/tacitt/scripts/journey-consolidation.mjs +224 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +143 -0
- package/skills/tacitt/scripts/journey-state.d.mts +42 -0
- package/skills/tacitt/scripts/journey-state.mjs +268 -0
- package/skills/tacitt/scripts/journey-tasks.d.mts +26 -0
- package/skills/tacitt/scripts/journey-tasks.mjs +183 -0
- package/skills/tacitt/scripts/journey.d.mts +38 -0
- package/skills/tacitt/scripts/journey.mjs +451 -0
- package/skills/tacitt/scripts/local-cli.d.mts +18 -0
- package/skills/tacitt/scripts/local-cli.mjs +116 -0
- package/skills/tacitt/scripts/privacy-validator.mjs +10350 -0
- package/skills/tacitt/scripts/review-stream-client.d.mts +22 -0
- package/skills/tacitt/scripts/review-stream-client.mjs +194 -0
- package/skills/tacitt/scripts/review.d.mts +87 -0
- package/skills/tacitt/scripts/review.mjs +1589 -0
- package/skills/tacitt/scripts/verify-cli.mjs +10 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { hash, optional } from './journey-state.mjs'
|
|
2
|
+
import { answers } from './journey-tasks.mjs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import {
|
|
5
|
+
validateClassificationJudgment,
|
|
6
|
+
validateClassificationProject,
|
|
7
|
+
} from './journey-classification-validation.mjs'
|
|
8
|
+
|
|
9
|
+
function inputFingerprint(view, rootIds) {
|
|
10
|
+
return hash({
|
|
11
|
+
snapshotId: view.snapshotId,
|
|
12
|
+
roots: rootIds,
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function classificationProgress(manifest) {
|
|
17
|
+
const classification = manifest.classification
|
|
18
|
+
if (!classification)
|
|
19
|
+
return {
|
|
20
|
+
generationComplete: false,
|
|
21
|
+
fixedProjects: 0,
|
|
22
|
+
remainingRoots: 0,
|
|
23
|
+
unresolvedRoots: 0,
|
|
24
|
+
generatedTasks: 0,
|
|
25
|
+
pendingAnswers: 0,
|
|
26
|
+
}
|
|
27
|
+
const pendingAnswers = (
|
|
28
|
+
await Promise.all(manifest.tasks.map((item) => optional(item.answer)))
|
|
29
|
+
).filter((answer) => !answer).length
|
|
30
|
+
const assigned = new Set(manifest.tasks.flatMap((item) => item.rootKeys ?? []))
|
|
31
|
+
const assignedProjects = new Set(manifest.tasks.flatMap((item) => item.projectKeys ?? []))
|
|
32
|
+
return {
|
|
33
|
+
generationComplete: classification.complete,
|
|
34
|
+
routingMs: classification.routingMs ?? null,
|
|
35
|
+
fixedProjects: classification.projects.length,
|
|
36
|
+
unresolvedRoots: classification.unknown.length,
|
|
37
|
+
remainingRoots:
|
|
38
|
+
classification.unknown.filter((root) => !assigned.has(root.rootKey)).length +
|
|
39
|
+
classification.projects
|
|
40
|
+
.filter((project) => !assignedProjects.has(project.key))
|
|
41
|
+
.reduce((sum, project) => sum + project.rootKeys.length, 0),
|
|
42
|
+
generatedTasks: manifest.tasks.length,
|
|
43
|
+
pendingAnswers,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Publish counts derived from validated per-root task receipts and proposal coverage. */
|
|
48
|
+
export async function publishClassificationProgress(
|
|
49
|
+
manifest,
|
|
50
|
+
view,
|
|
51
|
+
call,
|
|
52
|
+
{
|
|
53
|
+
status = 'running',
|
|
54
|
+
stage = manifest.consolidation ? 'proposal' : 'classification',
|
|
55
|
+
proposalRevision = null,
|
|
56
|
+
} = {},
|
|
57
|
+
) {
|
|
58
|
+
const classification = manifest.classification
|
|
59
|
+
if (!classification) return
|
|
60
|
+
const rootIds = classification.roots
|
|
61
|
+
.map((root) => root.rootSessionId)
|
|
62
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
63
|
+
const knownKeys = new Set(classification.projects.flatMap((project) => project.rootKeys))
|
|
64
|
+
const aiCompleted = new Set()
|
|
65
|
+
const completed = new Set()
|
|
66
|
+
const latestJudgments = new Set()
|
|
67
|
+
for (const item of manifest.tasks) {
|
|
68
|
+
if (item.evidenceRound > 0 && !(await optional(item.answer))) continue
|
|
69
|
+
if (item.projectKeys) {
|
|
70
|
+
const assigned = item.projectKeys
|
|
71
|
+
const projects = await answers([item], 'items').catch((error) => {
|
|
72
|
+
if (error.code === 'ENOENT') return []
|
|
73
|
+
throw error
|
|
74
|
+
})
|
|
75
|
+
const ids = projects.map((project) => project.projectId)
|
|
76
|
+
const expectedIds = classification.projects
|
|
77
|
+
.filter((project) => assigned.includes(project.key))
|
|
78
|
+
.map((project) => project.projectId)
|
|
79
|
+
if (
|
|
80
|
+
ids.length === expectedIds.length &&
|
|
81
|
+
new Set(ids).size === ids.length &&
|
|
82
|
+
expectedIds.every((id) => ids.includes(id))
|
|
83
|
+
) {
|
|
84
|
+
for (const project of projects) validateClassificationProject(project, project.projectId)
|
|
85
|
+
for (const project of classification.projects.filter((entry) =>
|
|
86
|
+
assigned.includes(entry.key),
|
|
87
|
+
)) {
|
|
88
|
+
for (const key of project.rootKeys) {
|
|
89
|
+
completed.add(key)
|
|
90
|
+
aiCompleted.add(key)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (!(await optional(item.answer))) continue
|
|
97
|
+
const judgments = await answers([item], 'judgments')
|
|
98
|
+
const answer = JSON.parse(await readFile(item.answer, 'utf8'))
|
|
99
|
+
const requestedEvidence = answer.evidenceRootKeys ?? []
|
|
100
|
+
if (
|
|
101
|
+
!Array.isArray(requestedEvidence) ||
|
|
102
|
+
requestedEvidence.some((key) => !item.rootKeys.includes(key))
|
|
103
|
+
)
|
|
104
|
+
throw new Error('INVALID_EVIDENCE_ROOTS')
|
|
105
|
+
if (item.evidenceRound > 0 && requestedEvidence.length)
|
|
106
|
+
throw new Error('EVIDENCE_ESCALATION_LIMIT')
|
|
107
|
+
const keys = judgments.map((judgment) => judgment.rootKey)
|
|
108
|
+
if (
|
|
109
|
+
keys.length !== item.rootKeys.length ||
|
|
110
|
+
new Set(keys).size !== keys.length ||
|
|
111
|
+
item.rootKeys.some((key) => !keys.includes(key))
|
|
112
|
+
)
|
|
113
|
+
continue
|
|
114
|
+
for (const judgment of judgments) validateClassificationJudgment(judgment, judgment.rootKey)
|
|
115
|
+
for (const key of keys) latestJudgments.add(key)
|
|
116
|
+
for (const key of requestedEvidence) latestJudgments.delete(key)
|
|
117
|
+
}
|
|
118
|
+
for (const key of latestJudgments) {
|
|
119
|
+
completed.add(key)
|
|
120
|
+
aiCompleted.add(key)
|
|
121
|
+
}
|
|
122
|
+
const grouped = new Set([...knownKeys].filter((key) => completed.has(key)))
|
|
123
|
+
const rootsByKey = new Map(classification.roots.map((root) => [root.key, root.rootSessionId]))
|
|
124
|
+
const completedRootIds = [...completed]
|
|
125
|
+
.map((key) => rootsByKey.get(key))
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
128
|
+
const groupedRootIds = [...grouped]
|
|
129
|
+
.map((key) => rootsByKey.get(key))
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
132
|
+
const aiCompletedRootIds = [...aiCompleted]
|
|
133
|
+
.map((key) => rootsByKey.get(key))
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
136
|
+
const startedAt = classification.startedAt ?? new Date().toISOString()
|
|
137
|
+
const updatedAt = new Date().toISOString()
|
|
138
|
+
await call('journey.classificationProgress', {
|
|
139
|
+
journeyId: view.journeyId,
|
|
140
|
+
kind: 'classification-progress',
|
|
141
|
+
id: 'current',
|
|
142
|
+
snapshotId: view.snapshotId,
|
|
143
|
+
baselineRevision: classification.baselineRevision,
|
|
144
|
+
proposalRevision,
|
|
145
|
+
inputFingerprint: inputFingerprint(view, rootIds),
|
|
146
|
+
stage,
|
|
147
|
+
status,
|
|
148
|
+
startedAt,
|
|
149
|
+
updatedAt,
|
|
150
|
+
elapsedMs: Math.max(0, Date.parse(updatedAt) - Date.parse(startedAt)),
|
|
151
|
+
rootIds,
|
|
152
|
+
completedRootIds,
|
|
153
|
+
groupedRootIds,
|
|
154
|
+
aiCompletedRootIds,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Fail only the exact running checkpoint already accepted by the backend. */
|
|
159
|
+
export async function publishFailedClassificationProgress(manifest, view, call) {
|
|
160
|
+
const classification = manifest.classification
|
|
161
|
+
const prior = view.classificationProgress
|
|
162
|
+
if (!classification || !prior || prior.status !== 'running') return
|
|
163
|
+
const rootIds = classification.roots
|
|
164
|
+
.map((root) => root.rootSessionId)
|
|
165
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
166
|
+
if (
|
|
167
|
+
view.revision !== classification.baselineRevision ||
|
|
168
|
+
prior.baselineRevision !== classification.baselineRevision ||
|
|
169
|
+
prior.startedAt !== classification.startedAt ||
|
|
170
|
+
prior.snapshotId !== view.snapshotId ||
|
|
171
|
+
prior.inputFingerprint !== inputFingerprint(view, rootIds) ||
|
|
172
|
+
JSON.stringify(prior.rootIds) !== JSON.stringify(rootIds)
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
const updatedAt = new Date().toISOString()
|
|
176
|
+
await call('journey.classificationProgress', {
|
|
177
|
+
journeyId: view.journeyId,
|
|
178
|
+
kind: 'classification-progress',
|
|
179
|
+
id: 'current',
|
|
180
|
+
snapshotId: prior.snapshotId,
|
|
181
|
+
baselineRevision: prior.baselineRevision,
|
|
182
|
+
proposalRevision: null,
|
|
183
|
+
inputFingerprint: prior.inputFingerprint,
|
|
184
|
+
stage: prior.stage,
|
|
185
|
+
status: 'failed',
|
|
186
|
+
startedAt: prior.startedAt,
|
|
187
|
+
updatedAt,
|
|
188
|
+
elapsedMs: Math.max(0, Date.parse(updatedAt) - Date.parse(prior.startedAt)),
|
|
189
|
+
rootIds: prior.rootIds,
|
|
190
|
+
completedRootIds: prior.completedRootIds,
|
|
191
|
+
groupedRootIds: prior.groupedRootIds,
|
|
192
|
+
aiCompletedRootIds: prior.aiCompletedRootIds,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function classificationTasks(
|
|
2
|
+
directory: string,
|
|
3
|
+
manifest: {
|
|
4
|
+
tasks: Array<Record<string, unknown>>
|
|
5
|
+
classification?: {
|
|
6
|
+
unknown: Array<{ metadata: Record<string, unknown> }>
|
|
7
|
+
[key: string]: unknown
|
|
8
|
+
}
|
|
9
|
+
[key: string]: unknown
|
|
10
|
+
},
|
|
11
|
+
view: unknown,
|
|
12
|
+
call: (procedure: string, input: Record<string, unknown>) => Promise<unknown>,
|
|
13
|
+
limit: number,
|
|
14
|
+
): Promise<{
|
|
15
|
+
tasks: Array<Record<string, unknown>>
|
|
16
|
+
stalled?: Array<{ id: string; ageMs: number }>
|
|
17
|
+
created: number
|
|
18
|
+
}>
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { optional, save } from './journey-state.mjs'
|
|
4
|
+
import { task, taskTokens } from './journey-tasks.mjs'
|
|
5
|
+
import { classificationProgress } from './journey-classification-progress.mjs'
|
|
6
|
+
|
|
7
|
+
const budget = 12000
|
|
8
|
+
/** A task unpublished this long after it was written has a stalled child (ADR 0207). */
|
|
9
|
+
const STALLED_MS = 900_000
|
|
10
|
+
// One bounded family per line; retain all evidence while avoiding repeated pretty-print whitespace.
|
|
11
|
+
const formatRoots = (data) =>
|
|
12
|
+
`{"knownProjects":[\n${data.knownProjects.map((row) => JSON.stringify(row)).join(',\n')}\n],"families":[\n${data.families.map((row) => JSON.stringify(row)).join(',\n')}\n]}`
|
|
13
|
+
export const classifyInstruction =
|
|
14
|
+
'Classify every assigned root family from compact metadata and partial structural project evidence. No privacy detection. Return {judgments:[{rootKey,projectName,workIntent,groupingBasis,targetProjectId?}],evidenceRootKeys:[]}. One judgment per rootKey; rootKey is an opaque handle, never invent or omit one. Before creating any new project, topic or Unsorted judgment, compare this root against every entry in knownProjects. Use targetProjectId only when that exact known project identity is supported; it must be the exact catalog projectId. Never select identity by display-name match. A targetProjectId becomes a fixed constraint in reconciliation: for genuinely cross-project roots with ambiguous or incomplete evidence, repository reference counts, change counts or apparent dominance cannot establish an owning project. Require direct evidence of this root’s owning intent before binding a target; otherwise omit targetProjectId, preserve the unresolved cross-project question in groupingBasis, and request one bounded evidence round only when it can resolve that question. An evidence request is not itself proof of ownership; bind only after its answer supports ownership. The known-project catalog is not exhaustive: an independently evidenced repository absent from it may establish a new project, including one root; omit targetProjectId for that new identity rather than treating catalog absence as Unsorted evidence. Exact complete supported repository evidence is positive identity evidence. Partial repository references are useful positive evidence but do not establish completeness; missing evidence is not negative proof. Independent content evidence can support a known project. Checking, using, connecting to, or integrating with a product/tool is not evidence that the root develops that product’s repository; require independent repository or content evidence before assigning project identity. A product topic may still be retained when roots genuinely share it. Scratch workspace name alone is insufficient, but do not ignore an exact repository reference or independent content evidence. projectName is a stable shared project or coherent theme name (<=240 chars), synthesized across the assigned roots, not a separate copy of each session title. Use the same name for roots with supported common identity or a genuinely shared topic. Use exactly Unsorted for one-off miscellaneous tasks with no supported project or meaningful shared theme; still preserve their concrete workIntent and groupingBasis so global reconciliation can reconsider them. Never create a project solely to keep a singleton. workIntent is a concrete short description of this root’s actual work, <=240 chars. If titleAvailable is false and no independent root-specific evidence establishes intent, explicitly state that intent is unknown; never invent an activity from a generated scratch/temp label or copy a sibling’s intent. Generated labels and sibling similarity alone cannot establish topic membership either. Keep such a root Unsorted with the missing evidence recorded, or request bounded evidence for a concrete membership-changing question. groupingBasis is concise evidence/uncertainty for grouping, <=200 chars. Do not produce Work descriptions, project types, visibility or recommendations here; those are assigned once during final grouping. Named ordinary source folders and corroborated project evidence can establish non-Git projects; generic home, scratch and temporary folders cannot. Titles alone do not prove a project. workspaceKind scratch, temp, home or unresolved is not an ordinary project folder: even a descriptive workspaceName may be generated from a chat title and cannot establish project identity. For those roots require independent project evidence before calling a new project; otherwise keep a theme or Unsorted and request bounded evidence only for a concrete membership-changing question. Preserve uncertainty in groupingBasis. Default evidenceRootKeys to empty. Request evidence only for a concrete unresolved identity question whose answer could change membership, and state that question in groupingBasis. Do not request evidence merely because a workspace lacks Git metadata, or blanket-request all roots of a named project your groupingBasis already supports. Missing project evidence can remain a provisional topic or Unsorted without proving a negative. A clear project remains independent with one root. Provisional topic evidence is useful across batches.'
|
|
15
|
+
|
|
16
|
+
const projectInstruction =
|
|
17
|
+
'Describe fixed repository identities only. Return {items:[{projectId,description,projectType,recommendation:{access,reason}}]}. Every assigned projectId exactly once. Membership, identity and name are fixed and cannot be reclassified. Description states intended work; reason may state the grouping evidence. No original content or privacy scan. Recommend drop only when evidence shows no useful retained purpose; heterogeneous work, uncertainty or incomplete evidence alone is insufficient. Public means visible on the Passport after privacy masking; Private stays hidden but still counts. Recommend public for showcase work that demonstrates the person’s own skills, such as side projects, open-source or public repositories, coursework, hackathons, personal sites and tools, unless evidence ties it to employer or client confidentiality, internal systems or sensitive personal matters; recommend private for those. Missing privacy evidence alone is not a reason for private. Explain uncertainty in the reason. Recommendations are not approvals. Types: work, side, coursework, personal, scratch, other; access: public, private, drop.'
|
|
18
|
+
|
|
19
|
+
const rootUniverse = (view) => {
|
|
20
|
+
const result = []
|
|
21
|
+
for (const work of view.works) {
|
|
22
|
+
for (const rootSessionId of work.rootSessionIds) {
|
|
23
|
+
result.push({ rootSessionId, workId: work.id })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function initialize(directory, manifest, view, call) {
|
|
30
|
+
if (manifest.classification) return
|
|
31
|
+
const works = new Map(view.works.map((work) => [work.id, work]))
|
|
32
|
+
const universe = rootUniverse(view)
|
|
33
|
+
const rootWork = new Map(universe.map((entry) => [entry.rootSessionId, entry.workId]))
|
|
34
|
+
const startedAt = new Date().toISOString()
|
|
35
|
+
const routingStart = performance.now()
|
|
36
|
+
const page = await call('journey.catalog', {
|
|
37
|
+
journeyId: view.journeyId,
|
|
38
|
+
after: 0,
|
|
39
|
+
limit: 1,
|
|
40
|
+
includeClassificationRouting: true,
|
|
41
|
+
includeProjectEvidence: true,
|
|
42
|
+
})
|
|
43
|
+
const routingMs = performance.now() - routingStart
|
|
44
|
+
const selected = page.classificationRouting
|
|
45
|
+
if (!selected) throw new Error('CLASSIFICATION_ROUTING_MISSING')
|
|
46
|
+
const known = selected.knownProjects ?? []
|
|
47
|
+
const unresolved = selected.unresolvedRoots ?? []
|
|
48
|
+
const assignments = selected.assignments ?? []
|
|
49
|
+
const knownRootIds = new Set(known.flatMap((project) => project.rootSessionIds))
|
|
50
|
+
const unresolvedIds = new Set(unresolved.map((root) => root.rootSessionId))
|
|
51
|
+
const assignmentByRoot = new Map(assignments.map((entry) => [entry.rootSessionId, entry]))
|
|
52
|
+
if (
|
|
53
|
+
knownRootIds.size !== known.flatMap((project) => project.rootSessionIds).length ||
|
|
54
|
+
unresolvedIds.size !== unresolved.length ||
|
|
55
|
+
assignments.length !== universe.length ||
|
|
56
|
+
assignmentByRoot.size !== universe.length ||
|
|
57
|
+
universe.some((root) => !assignmentByRoot.has(root.rootSessionId)) ||
|
|
58
|
+
knownRootIds.size + unresolvedIds.size !== universe.length ||
|
|
59
|
+
universe.some(
|
|
60
|
+
(root) => !knownRootIds.has(root.rootSessionId) && !unresolvedIds.has(root.rootSessionId),
|
|
61
|
+
) ||
|
|
62
|
+
[...knownRootIds].some((id) => !rootWork.has(id)) ||
|
|
63
|
+
[...unresolvedIds].some((id) => !rootWork.has(id)) ||
|
|
64
|
+
known.some((project) =>
|
|
65
|
+
project.rootSessionIds.some(
|
|
66
|
+
(id) => assignmentByRoot.get(id)?.projectId !== project.repositoryId,
|
|
67
|
+
),
|
|
68
|
+
) ||
|
|
69
|
+
unresolved.some((root) => assignmentByRoot.get(root.rootSessionId)?.projectId !== null)
|
|
70
|
+
)
|
|
71
|
+
throw new Error('CLASSIFICATION_ROUTING_COVERAGE_INVALID')
|
|
72
|
+
|
|
73
|
+
const roots = universe.map((entry, index) => ({
|
|
74
|
+
key: `r${index + 1}`,
|
|
75
|
+
...entry,
|
|
76
|
+
}))
|
|
77
|
+
const byId = new Map(roots.map((root) => [root.rootSessionId, root]))
|
|
78
|
+
const projects = known.map((project, index) => ({
|
|
79
|
+
projectId: project.repositoryId,
|
|
80
|
+
projectName: project.repositoryName,
|
|
81
|
+
rootKeys: project.rootSessionIds.map((id) => byId.get(id)?.key),
|
|
82
|
+
metadata: project,
|
|
83
|
+
key: `p${String(index + 1).padStart(4, '0')}`,
|
|
84
|
+
}))
|
|
85
|
+
if (projects.some((project) => project.rootKeys.some((key) => !key)))
|
|
86
|
+
throw new Error('CLASSIFICATION_ROUTING_COVERAGE_INVALID')
|
|
87
|
+
const projectRootKeys = new Set(projects.flatMap((project) => project.rootKeys))
|
|
88
|
+
const unknown = unresolved.map((item) => {
|
|
89
|
+
const root = byId.get(item.rootSessionId)
|
|
90
|
+
if (!root || projectRootKeys.has(root.key))
|
|
91
|
+
throw new Error('CLASSIFICATION_ROUTING_COVERAGE_INVALID')
|
|
92
|
+
const work = works.get(root.workId)
|
|
93
|
+
if (!work) throw new Error('CLASSIFICATION_ROUTING_COVERAGE_INVALID')
|
|
94
|
+
return {
|
|
95
|
+
rootKey: root.key,
|
|
96
|
+
metadata: {
|
|
97
|
+
workspaceKind: item.workspaceKind,
|
|
98
|
+
workspaceName: item.workspaceName,
|
|
99
|
+
harness: item.harness,
|
|
100
|
+
titleAvailable: item.titleAvailable,
|
|
101
|
+
memberCount: item.memberCount,
|
|
102
|
+
titles: item.titles,
|
|
103
|
+
projectEvidence: item.projectEvidence ?? null,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
if (projectRootKeys.size + unknown.length !== roots.length)
|
|
108
|
+
throw new Error('CLASSIFICATION_ROUTING_COVERAGE_INVALID')
|
|
109
|
+
manifest.classification = {
|
|
110
|
+
roots,
|
|
111
|
+
projects,
|
|
112
|
+
unknown,
|
|
113
|
+
routingAssignments: assignments,
|
|
114
|
+
evidence: [],
|
|
115
|
+
complete: false,
|
|
116
|
+
startedAt,
|
|
117
|
+
routingMs,
|
|
118
|
+
baselineRevision: view.revision,
|
|
119
|
+
}
|
|
120
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function pendingTasks(manifest) {
|
|
124
|
+
const result = []
|
|
125
|
+
for (const item of manifest.tasks) if (!(await optional(item.answer))) result.push(item)
|
|
126
|
+
return result.sort(
|
|
127
|
+
(left, right) =>
|
|
128
|
+
Number(Boolean(right.rootKeys?.length)) - Number(Boolean(left.rootKeys?.length)),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function classificationTasks(directory, manifest, view, call, limit) {
|
|
133
|
+
await initialize(directory, manifest, view, call)
|
|
134
|
+
const state = manifest.classification
|
|
135
|
+
const created = []
|
|
136
|
+
const availableSlots = Math.max(0, Math.floor(limit))
|
|
137
|
+
const knownProjects = state.projects.map(({ projectId, projectName }) => ({
|
|
138
|
+
projectId,
|
|
139
|
+
projectName,
|
|
140
|
+
}))
|
|
141
|
+
// Existing assignments are immutable. Allocate the remaining slots between
|
|
142
|
+
// both families by estimated payload before constructing any new tasks.
|
|
143
|
+
const outstandingProjects = state.projects.filter(
|
|
144
|
+
(project) => !manifest.tasks.some((item) => item.projectKeys?.includes(project.key)),
|
|
145
|
+
)
|
|
146
|
+
const outstandingRootKeys = new Set(state.unknown.map((item) => item.rootKey))
|
|
147
|
+
for (const entry of manifest.tasks)
|
|
148
|
+
for (const key of entry.rootKeys ?? []) outstandingRootKeys.delete(key)
|
|
149
|
+
const outstandingRoots = state.unknown.filter((item) => outstandingRootKeys.has(item.rootKey))
|
|
150
|
+
const targetSlots = Math.min(availableSlots, outstandingProjects.length + outstandingRoots.length)
|
|
151
|
+
const projectBaseCost = taskTokens(directory, 'projects-estimate', projectInstruction, {
|
|
152
|
+
projects: [],
|
|
153
|
+
})
|
|
154
|
+
const rootBaseCost = taskTokens(
|
|
155
|
+
directory,
|
|
156
|
+
'roots-estimate',
|
|
157
|
+
classifyInstruction,
|
|
158
|
+
{ knownProjects, families: [] },
|
|
159
|
+
formatRoots,
|
|
160
|
+
)
|
|
161
|
+
const projectCosts = new Map(
|
|
162
|
+
outstandingProjects.map((project) => [
|
|
163
|
+
project.key,
|
|
164
|
+
Math.max(
|
|
165
|
+
1,
|
|
166
|
+
taskTokens(directory, 'projects-estimate', projectInstruction, {
|
|
167
|
+
projects: [
|
|
168
|
+
{
|
|
169
|
+
projectId: project.projectId,
|
|
170
|
+
projectName: project.projectName,
|
|
171
|
+
workspaceCount: project.metadata.workspaceIds.length,
|
|
172
|
+
titles: project.metadata.titles,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
}) - projectBaseCost,
|
|
176
|
+
),
|
|
177
|
+
]),
|
|
178
|
+
)
|
|
179
|
+
const rootCosts = new Map(
|
|
180
|
+
outstandingRoots.map((root) => [
|
|
181
|
+
root.rootKey,
|
|
182
|
+
Math.max(
|
|
183
|
+
1,
|
|
184
|
+
taskTokens(
|
|
185
|
+
directory,
|
|
186
|
+
'roots-estimate',
|
|
187
|
+
classifyInstruction,
|
|
188
|
+
{ knownProjects, families: [root] },
|
|
189
|
+
formatRoots,
|
|
190
|
+
) - rootBaseCost,
|
|
191
|
+
),
|
|
192
|
+
]),
|
|
193
|
+
)
|
|
194
|
+
const projectCost = (project) => projectCosts.get(project.key) ?? 1
|
|
195
|
+
const rootCost = (root) => rootCosts.get(root.rootKey) ?? 1
|
|
196
|
+
const projectTotal = outstandingProjects.reduce((sum, item) => sum + projectCost(item), 0)
|
|
197
|
+
const rootTotal = outstandingRoots.reduce((sum, item) => sum + rootCost(item), 0)
|
|
198
|
+
let projectSlots = 0
|
|
199
|
+
let rootSlots = 0
|
|
200
|
+
if (targetSlots === 1) {
|
|
201
|
+
if (projectTotal >= rootTotal) projectSlots = 1
|
|
202
|
+
else rootSlots = 1
|
|
203
|
+
} else if (targetSlots > 1) {
|
|
204
|
+
if (outstandingProjects.length && outstandingRoots.length) {
|
|
205
|
+
projectSlots = Math.max(
|
|
206
|
+
1,
|
|
207
|
+
Math.min(
|
|
208
|
+
outstandingProjects.length,
|
|
209
|
+
Math.round((targetSlots * projectTotal) / (projectTotal + rootTotal)),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
rootSlots = targetSlots - projectSlots
|
|
213
|
+
if (rootSlots < 1) {
|
|
214
|
+
rootSlots = 1
|
|
215
|
+
projectSlots -= 1
|
|
216
|
+
}
|
|
217
|
+
if (rootSlots > outstandingRoots.length) {
|
|
218
|
+
projectSlots += rootSlots - outstandingRoots.length
|
|
219
|
+
rootSlots = outstandingRoots.length
|
|
220
|
+
}
|
|
221
|
+
if (projectSlots > outstandingProjects.length) {
|
|
222
|
+
rootSlots += projectSlots - outstandingProjects.length
|
|
223
|
+
projectSlots = outstandingProjects.length
|
|
224
|
+
}
|
|
225
|
+
} else if (outstandingProjects.length) projectSlots = targetSlots
|
|
226
|
+
else rootSlots = targetSlots
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const makeProjectTasks = async () => {
|
|
230
|
+
const queue = [...outstandingProjects]
|
|
231
|
+
for (let slot = 0; slot < projectSlots && queue.length; slot += 1) {
|
|
232
|
+
const slotsLeft = projectSlots - slot
|
|
233
|
+
const maxItems = Math.max(1, queue.length - slotsLeft + 1)
|
|
234
|
+
const slotBudget = Math.ceil(
|
|
235
|
+
queue.reduce((sum, item) => sum + projectCost(item), 0) / slotsLeft,
|
|
236
|
+
)
|
|
237
|
+
const projects = []
|
|
238
|
+
let payload = 0
|
|
239
|
+
while (queue.length) {
|
|
240
|
+
if (projects.length >= maxItems) break
|
|
241
|
+
const candidate = queue[0]
|
|
242
|
+
const projectInput = {
|
|
243
|
+
projectId: candidate.projectId,
|
|
244
|
+
projectName: candidate.projectName,
|
|
245
|
+
workspaceCount: candidate.metadata.workspaceIds.length,
|
|
246
|
+
titles: candidate.metadata.titles,
|
|
247
|
+
}
|
|
248
|
+
const candidateInput = { projects: [...projects, projectInput] }
|
|
249
|
+
const cost = taskTokens(
|
|
250
|
+
directory,
|
|
251
|
+
`projects-${manifest.tasks.length}`,
|
|
252
|
+
projectInstruction,
|
|
253
|
+
candidateInput,
|
|
254
|
+
)
|
|
255
|
+
if (projects.length && (cost > budget || payload >= slotBudget)) break
|
|
256
|
+
if (!projects.length && cost > budget) throw new Error('CLASSIFICATION_PROJECT_TOO_LARGE')
|
|
257
|
+
projects.push(projectInput)
|
|
258
|
+
payload += projectCost(candidate)
|
|
259
|
+
queue.shift()
|
|
260
|
+
}
|
|
261
|
+
if (!projects.length) break
|
|
262
|
+
const keys = projects.map((item) => queueProjectKey(state.projects, item.projectId))
|
|
263
|
+
const item = await task(directory, `projects-${manifest.tasks.length}`, projectInstruction, {
|
|
264
|
+
projects,
|
|
265
|
+
})
|
|
266
|
+
const assigned = { ...item, projectKeys: keys }
|
|
267
|
+
manifest.tasks.push(assigned)
|
|
268
|
+
created.push(assigned)
|
|
269
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const makeRootTasks = async () => {
|
|
274
|
+
const rootWorkByKey = new Map(state.roots.map((root) => [root.key, root.workId]))
|
|
275
|
+
const queue = outstandingRoots
|
|
276
|
+
.map((root, index) => ({ root, index }))
|
|
277
|
+
.sort(
|
|
278
|
+
(a, b) =>
|
|
279
|
+
String(rootWorkByKey.get(a.root.rootKey)).localeCompare(
|
|
280
|
+
String(rootWorkByKey.get(b.root.rootKey)),
|
|
281
|
+
) || a.index - b.index,
|
|
282
|
+
)
|
|
283
|
+
.map(({ root }) => root)
|
|
284
|
+
for (let slot = 0; slot < rootSlots && queue.length; slot += 1) {
|
|
285
|
+
const slotsLeft = rootSlots - slot
|
|
286
|
+
const maxItems = Math.max(1, queue.length - slotsLeft + 1)
|
|
287
|
+
const slotBudget = Math.ceil(queue.reduce((sum, item) => sum + rootCost(item), 0) / slotsLeft)
|
|
288
|
+
const batch = []
|
|
289
|
+
let payload = 0
|
|
290
|
+
while (queue.length) {
|
|
291
|
+
if (batch.length >= maxItems) break
|
|
292
|
+
const candidate = [...batch, queue[0]]
|
|
293
|
+
const cost = taskTokens(
|
|
294
|
+
directory,
|
|
295
|
+
`roots-${manifest.tasks.length}`,
|
|
296
|
+
classifyInstruction,
|
|
297
|
+
{ knownProjects, families: candidate },
|
|
298
|
+
formatRoots,
|
|
299
|
+
)
|
|
300
|
+
if (batch.length && (cost > budget || payload >= slotBudget)) break
|
|
301
|
+
if (!batch.length && cost > budget) throw new Error('CLASSIFICATION_ROOT_TOO_LARGE')
|
|
302
|
+
const root = queue.shift()
|
|
303
|
+
batch.push(root)
|
|
304
|
+
payload += rootCost(root)
|
|
305
|
+
}
|
|
306
|
+
if (!batch.length) break
|
|
307
|
+
const item = await task(
|
|
308
|
+
directory,
|
|
309
|
+
`roots-${manifest.tasks.length}`,
|
|
310
|
+
classifyInstruction,
|
|
311
|
+
{ knownProjects, families: batch },
|
|
312
|
+
formatRoots,
|
|
313
|
+
)
|
|
314
|
+
const assigned = { ...item, rootKeys: batch.map((root) => root.rootKey), evidenceRound: 0 }
|
|
315
|
+
manifest.tasks.push(assigned)
|
|
316
|
+
created.push(assigned)
|
|
317
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const queueProjectKey = (projects, projectId) =>
|
|
322
|
+
projects.find((item) => item.projectId === projectId)?.key
|
|
323
|
+
await makeProjectTasks()
|
|
324
|
+
await makeRootTasks()
|
|
325
|
+
const assignedRootKeys = new Set(manifest.tasks.flatMap((item) => item.rootKeys ?? []))
|
|
326
|
+
state.complete =
|
|
327
|
+
state.projects.every((project) =>
|
|
328
|
+
manifest.tasks.some((item) => item.projectKeys?.includes(project.key)),
|
|
329
|
+
) && state.unknown.every((root) => assignedRootKeys.has(root.rootKey))
|
|
330
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
331
|
+
const tasks = await pendingTasks(manifest)
|
|
332
|
+
const now = Date.now()
|
|
333
|
+
const stalled = []
|
|
334
|
+
for (const item of tasks) {
|
|
335
|
+
const ageMs = now - (await stat(item.path)).mtimeMs
|
|
336
|
+
if (ageMs > STALLED_MS) stalled.push({ id: item.id, ageMs: Math.round(ageMs) })
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
stage: 'classification',
|
|
340
|
+
tasks,
|
|
341
|
+
...(stalled.length ? { stalled } : {}),
|
|
342
|
+
created: created.length,
|
|
343
|
+
...(await classificationProgress(manifest)),
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function validateClassificationJudgment(value: unknown, key: string): void
|
|
2
|
+
export function validateClassificationProject(value: unknown, projectId: string): void
|
|
3
|
+
export function compileAssignments(
|
|
4
|
+
candidates: Array<{
|
|
5
|
+
key: string
|
|
6
|
+
projectName: string
|
|
7
|
+
roots: Array<[string, string, string] | [string, string, string, string]>
|
|
8
|
+
}>,
|
|
9
|
+
assignments: Record<string, string>,
|
|
10
|
+
overrides?: Array<{ rootKey: string; groupId: string }>,
|
|
11
|
+
): Map<string, string>
|
|
12
|
+
export function validateConsolidationAnswer(
|
|
13
|
+
answer: unknown,
|
|
14
|
+
descriptor: {
|
|
15
|
+
candidates: Array<{
|
|
16
|
+
roots: Array<[string, string, string] | [string, string, string, string]>
|
|
17
|
+
}>
|
|
18
|
+
projectCatalog: unknown[]
|
|
19
|
+
},
|
|
20
|
+
): { code: string | null; details: string[] }
|