@gitroll/tacitt-cli 0.0.0-stage → 0.2.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-B8DAiQ6Z.mjs +71 -0
- package/dist/agent-setup-worker.mjs +56 -0
- package/dist/cli.mjs +1442 -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-waS5FnN5.mjs +84 -0
- package/dist/heap-1FYhGds4.mjs +75 -0
- package/dist/heap-BHcvxWCw.mjs +7265 -0
- package/dist/inspection-worker.mjs +57 -0
- package/dist/job-J3NFw3MH.mjs +194 -0
- package/dist/locate-U3-nkird.mjs +10670 -0
- package/dist/messages-D3m3bhva.mjs +40 -0
- package/dist/model-BJdTLNjT.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-BA0XCwnR.mjs +14869 -0
- package/dist/run-BeLHsZWb.mjs +916 -0
- package/dist/session-process.mjs +90 -0
- package/dist/src-B5SArpVY.mjs +6601 -0
- package/dist/src-CC95LYYp.mjs +12985 -0
- package/dist/src-k0Ya1Bri.mjs +16422 -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-Bjos73wK.js +67 -0
- package/dist/web/assets/index-Bl4C2b2_.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 +44 -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 +54 -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 +278 -0
- package/skills/tacitt/scripts/describe.mjs +55 -0
- package/skills/tacitt/scripts/journey-card-consolidation.d.mts +17 -0
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +209 -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 +195 -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 +240 -0
- package/skills/tacitt/scripts/journey-classification.mjs +145 -0
- package/skills/tacitt/scripts/journey-consolidation-submit.mjs +227 -0
- package/skills/tacitt/scripts/journey-consolidation.d.mts +14 -0
- package/skills/tacitt/scripts/journey-consolidation.mjs +227 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +143 -0
- package/skills/tacitt/scripts/journey-state.d.mts +43 -0
- package/skills/tacitt/scripts/journey-state.mjs +294 -0
- package/skills/tacitt/scripts/journey-tasks.d.mts +26 -0
- package/skills/tacitt/scripts/journey-tasks.mjs +185 -0
- package/skills/tacitt/scripts/journey.d.mts +40 -0
- package/skills/tacitt/scripts/journey.mjs +592 -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 +10369 -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 +1610 -0
- package/skills/tacitt/scripts/verify-cli.mjs +10 -0
|
@@ -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[] }
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const types = ['work', 'side', 'coursework', 'personal', 'scratch', 'other']
|
|
2
|
+
const access = ['public', 'private', 'drop']
|
|
3
|
+
const unknownField = (value, allowed) => {
|
|
4
|
+
const field = Object.keys(value).find((name) => !allowed(name))
|
|
5
|
+
return field && `unknown field ${field}`
|
|
6
|
+
}
|
|
7
|
+
const text = (value, max) =>
|
|
8
|
+
typeof value === 'string' && value.trim().length > 0 && value.length <= max
|
|
9
|
+
|
|
10
|
+
export function validateClassificationJudgment(value, key) {
|
|
11
|
+
const problem = !value
|
|
12
|
+
? 'missing'
|
|
13
|
+
: value.rootKey !== key
|
|
14
|
+
? 'rootKey does not match'
|
|
15
|
+
: !text(value.projectName, 240)
|
|
16
|
+
? 'projectName must be 1-240 characters'
|
|
17
|
+
: !text(value.workIntent, 240)
|
|
18
|
+
? 'workIntent must be 1-240 characters'
|
|
19
|
+
: !text(value.groupingBasis, 200)
|
|
20
|
+
? 'groupingBasis must be 1-200 characters'
|
|
21
|
+
: value.targetProjectId !== undefined &&
|
|
22
|
+
(typeof value.targetProjectId !== 'string' || !value.targetProjectId)
|
|
23
|
+
? 'targetProjectId must be a non-empty string'
|
|
24
|
+
: unknownField(value, (field) =>
|
|
25
|
+
[
|
|
26
|
+
'rootKey',
|
|
27
|
+
'projectName',
|
|
28
|
+
'workIntent',
|
|
29
|
+
'groupingBasis',
|
|
30
|
+
'targetProjectId',
|
|
31
|
+
].includes(field),
|
|
32
|
+
)
|
|
33
|
+
if (problem)
|
|
34
|
+
throw new Error('CLASSIFICATION_JUDGMENT_INVALID', {
|
|
35
|
+
cause: problem,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validateClassificationProject(value, projectId) {
|
|
40
|
+
const problem = !value
|
|
41
|
+
? 'missing'
|
|
42
|
+
: value.projectId !== projectId
|
|
43
|
+
? 'projectId does not match'
|
|
44
|
+
: !text(value.description, 1000)
|
|
45
|
+
? 'description must be 1-1000 characters'
|
|
46
|
+
: !types.includes(value.projectType)
|
|
47
|
+
? `projectType must be one of ${types.join(', ')}`
|
|
48
|
+
: !access.includes(value.recommendation?.access)
|
|
49
|
+
? `recommendation.access must be one of ${access.join(', ')}`
|
|
50
|
+
: !text(value.recommendation?.reason, 200)
|
|
51
|
+
? 'recommendation.reason must be 1-200 characters'
|
|
52
|
+
: unknownField(value, (field) =>
|
|
53
|
+
['projectId', 'description', 'projectType', 'recommendation'].includes(field),
|
|
54
|
+
)
|
|
55
|
+
if (problem)
|
|
56
|
+
throw new Error('PROJECT_METADATA_INVALID', {
|
|
57
|
+
cause: problem,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateConsolidationAnswer(answer, descriptor) {
|
|
62
|
+
const details = []
|
|
63
|
+
const groups = answer?.groups
|
|
64
|
+
const assignments = answer?.assignments
|
|
65
|
+
const candidates = descriptor?.candidates
|
|
66
|
+
const candidateKeys = new Set()
|
|
67
|
+
const descriptorRoots = new Set()
|
|
68
|
+
if (!Array.isArray(candidates)) details.push('descriptor.candidates is missing')
|
|
69
|
+
else
|
|
70
|
+
candidates.forEach((candidate, index) => {
|
|
71
|
+
if (
|
|
72
|
+
!candidate ||
|
|
73
|
+
typeof candidate.key !== 'string' ||
|
|
74
|
+
!candidate.key ||
|
|
75
|
+
typeof candidate.projectName !== 'string' ||
|
|
76
|
+
!Array.isArray(candidate.roots) ||
|
|
77
|
+
candidate.roots.some(
|
|
78
|
+
(row) =>
|
|
79
|
+
!Array.isArray(row) ||
|
|
80
|
+
(row.length !== 3 && row.length !== 4) ||
|
|
81
|
+
typeof row[0] !== 'string' ||
|
|
82
|
+
(row.length === 4 && (typeof row[3] !== 'string' || !row[3])),
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
details.push(`descriptor.candidates[${index}] is invalid`)
|
|
86
|
+
else {
|
|
87
|
+
if (candidateKeys.has(candidate.key))
|
|
88
|
+
details.push(`descriptor.candidates[${index}].key is duplicated`)
|
|
89
|
+
candidateKeys.add(candidate.key)
|
|
90
|
+
for (const row of candidate.roots) {
|
|
91
|
+
if (descriptorRoots.has(row[0]))
|
|
92
|
+
details.push(`descriptor.candidates[${index}] repeats a root`)
|
|
93
|
+
descriptorRoots.add(row[0])
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
const rootOrder = Array.isArray(candidates)
|
|
98
|
+
? candidates.flatMap((candidate) =>
|
|
99
|
+
Array.isArray(candidate?.roots)
|
|
100
|
+
? candidate.roots.map((row) => (Array.isArray(row) ? row[0] : null))
|
|
101
|
+
: [],
|
|
102
|
+
)
|
|
103
|
+
: null
|
|
104
|
+
const catalog = descriptor?.projectCatalog
|
|
105
|
+
if (!Array.isArray(groups)) details.push('groups must be an array')
|
|
106
|
+
if (!assignments || typeof assignments !== 'object' || Array.isArray(assignments))
|
|
107
|
+
details.push('assignments must be an object keyed by candidate key')
|
|
108
|
+
if (!Array.isArray(catalog)) details.push('descriptor.projectCatalog is missing')
|
|
109
|
+
if (Array.isArray(catalog) && Array.isArray(candidates))
|
|
110
|
+
for (const candidate of candidates)
|
|
111
|
+
for (const row of candidate.roots ?? [])
|
|
112
|
+
if (row.length === 4 && !catalog.some((entry) => entry.projectId === row[3]))
|
|
113
|
+
details.push(`root ${row[0]} has an unknown targetProjectId`)
|
|
114
|
+
if (assignments && typeof assignments === 'object' && !Array.isArray(assignments)) {
|
|
115
|
+
for (const key of candidateKeys)
|
|
116
|
+
if (!Object.hasOwn(assignments, key))
|
|
117
|
+
details.push(`assignments is missing candidate key ${key}`)
|
|
118
|
+
for (const key of Object.keys(assignments))
|
|
119
|
+
if (!candidateKeys.has(key)) details.push(`assignments contains unknown candidate key ${key}`)
|
|
120
|
+
}
|
|
121
|
+
const ids = new Set()
|
|
122
|
+
const groupIndexes = new Map()
|
|
123
|
+
const names = new Map()
|
|
124
|
+
if (Array.isArray(groups))
|
|
125
|
+
groups.forEach((group, index) => {
|
|
126
|
+
if (!group || typeof group.id !== 'string' || !group.id.trim()) {
|
|
127
|
+
details.push(`groups[${index}].id must be a non-empty string`)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if (ids.has(group.id))
|
|
131
|
+
details.push(`groups[${index}].id duplicates groups[${groupIndexes.get(group.id)}].id`)
|
|
132
|
+
if (
|
|
133
|
+
Object.keys(group).some(
|
|
134
|
+
(field) => !['id', 'projectName', 'groupKind', 'targetProjectId'].includes(field),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
details.push(`groups[${index}] contains unsupported fields`)
|
|
138
|
+
if (
|
|
139
|
+
group.targetProjectId !== undefined &&
|
|
140
|
+
(typeof group.targetProjectId !== 'string' || !group.targetProjectId)
|
|
141
|
+
)
|
|
142
|
+
details.push(`groups[${index}].targetProjectId is invalid`)
|
|
143
|
+
ids.add(group.id)
|
|
144
|
+
groupIndexes.set(group.id, index)
|
|
145
|
+
if (!['project', 'topic', 'unsorted'].includes(group.groupKind))
|
|
146
|
+
details.push(`groups[${index}].groupKind is invalid`)
|
|
147
|
+
if (!text(group.projectName, 240)) details.push(`groups[${index}].projectName is invalid`)
|
|
148
|
+
if (
|
|
149
|
+
group.targetProjectId &&
|
|
150
|
+
Array.isArray(catalog) &&
|
|
151
|
+
!catalog.some((entry) => entry.projectId === group.targetProjectId)
|
|
152
|
+
)
|
|
153
|
+
details.push(`groups[${index}].targetProjectId is unknown`)
|
|
154
|
+
if (group.targetProjectId && group.groupKind !== 'project')
|
|
155
|
+
details.push(`groups[${index}].targetProjectId requires groupKind project`)
|
|
156
|
+
if (
|
|
157
|
+
group.groupKind !== 'unsorted' &&
|
|
158
|
+
typeof group.projectName === 'string' &&
|
|
159
|
+
group.projectName.trim()
|
|
160
|
+
) {
|
|
161
|
+
const name = group.projectName.normalize('NFC').toLocaleLowerCase('en-US')
|
|
162
|
+
if (
|
|
163
|
+
Array.isArray(catalog) &&
|
|
164
|
+
catalog.some(
|
|
165
|
+
(entry) => entry.projectName.normalize('NFC').toLocaleLowerCase('en-US') === name,
|
|
166
|
+
) &&
|
|
167
|
+
!group.targetProjectId
|
|
168
|
+
)
|
|
169
|
+
details.push(
|
|
170
|
+
`groups[${index}].projectName conflicts with a known project; targetProjectId is required`,
|
|
171
|
+
)
|
|
172
|
+
const identity = JSON.stringify([group.targetProjectId ?? null, name])
|
|
173
|
+
const prior = names.get(identity)
|
|
174
|
+
if (prior)
|
|
175
|
+
details.push(`groups[${index}].projectName duplicates groups[${prior.index}].projectName`)
|
|
176
|
+
else names.set(identity, { index, targetProjectId: group.targetProjectId })
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
if (assignments && typeof assignments === 'object' && !Array.isArray(assignments))
|
|
180
|
+
for (const [key, id] of Object.entries(assignments))
|
|
181
|
+
if (typeof id !== 'string' || !ids.has(id))
|
|
182
|
+
details.push(`assignments[${key}] names an unknown group id`)
|
|
183
|
+
const roots = new Set(rootOrder ?? [])
|
|
184
|
+
const overrides = answer?.overrides ?? []
|
|
185
|
+
if (!Array.isArray(overrides)) details.push('overrides must be an array when present')
|
|
186
|
+
const overridden = new Set()
|
|
187
|
+
if (Array.isArray(overrides))
|
|
188
|
+
overrides.forEach((override, index) => {
|
|
189
|
+
if (!override || typeof override.rootKey !== 'string' || !roots.has(override.rootKey))
|
|
190
|
+
details.push(`overrides[${index}].rootKey is unknown`)
|
|
191
|
+
else if (overridden.has(override.rootKey))
|
|
192
|
+
details.push(`overrides[${index}].rootKey is duplicated`)
|
|
193
|
+
else overridden.add(override.rootKey)
|
|
194
|
+
if (!override || typeof override.groupId !== 'string' || !ids.has(override.groupId))
|
|
195
|
+
details.push(`overrides[${index}].groupId names an unknown group id`)
|
|
196
|
+
})
|
|
197
|
+
if (
|
|
198
|
+
Array.isArray(candidates) &&
|
|
199
|
+
assignments &&
|
|
200
|
+
typeof assignments === 'object' &&
|
|
201
|
+
!Array.isArray(assignments) &&
|
|
202
|
+
Array.isArray(overrides) &&
|
|
203
|
+
candidates.every((candidate) => Array.isArray(candidate?.roots))
|
|
204
|
+
) {
|
|
205
|
+
const finalIds = new Map()
|
|
206
|
+
const requiredTargets = new Map()
|
|
207
|
+
candidates.forEach((candidate) => {
|
|
208
|
+
for (const row of candidate.roots) {
|
|
209
|
+
finalIds.set(
|
|
210
|
+
row[0],
|
|
211
|
+
Object.hasOwn(assignments, candidate.key) ? assignments[candidate.key] : null,
|
|
212
|
+
)
|
|
213
|
+
if (row[3]) requiredTargets.set(row[0], row[3])
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
for (const override of overrides)
|
|
217
|
+
if (typeof override?.rootKey === 'string' && typeof override?.groupId === 'string')
|
|
218
|
+
finalIds.set(override.rootKey, override.groupId)
|
|
219
|
+
for (const [rootKey, targetProjectId] of requiredTargets) {
|
|
220
|
+
const group = Array.isArray(groups)
|
|
221
|
+
? groups.find((entry) => entry.id === finalIds.get(rootKey))
|
|
222
|
+
: null
|
|
223
|
+
if (group && group.targetProjectId !== targetProjectId)
|
|
224
|
+
details.push(`root ${rootKey} must remain assigned to targetProjectId ${targetProjectId}`)
|
|
225
|
+
}
|
|
226
|
+
const populated = new Set(finalIds.values())
|
|
227
|
+
for (const [id, index] of groupIndexes)
|
|
228
|
+
if (!populated.has(id)) details.push(`groups[${index}] has no roots after overrides`)
|
|
229
|
+
}
|
|
230
|
+
return { code: details.length ? 'CLASSIFICATION_CONSOLIDATION_INVALID' : null, details }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function compileAssignments(candidates, assignments, overrides = []) {
|
|
234
|
+
const result = new Map()
|
|
235
|
+
for (const candidate of candidates)
|
|
236
|
+
for (const row of candidate.roots)
|
|
237
|
+
if (Object.hasOwn(assignments, candidate.key)) result.set(row[0], assignments[candidate.key])
|
|
238
|
+
for (const override of overrides) result.set(override.rootKey, override.groupId)
|
|
239
|
+
return result
|
|
240
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { optional, save } from './journey-state.mjs'
|
|
4
|
+
import { answers, task, taskTokens } from './journey-tasks.mjs'
|
|
5
|
+
import { classifyInstruction } from './journey-classification-tasks.mjs'
|
|
6
|
+
|
|
7
|
+
export const classificationFormat = 13
|
|
8
|
+
|
|
9
|
+
/** Requested evidence is bounded to two pages and generates replacement judgments. */
|
|
10
|
+
export async function classificationEvidence(directory, manifest, view, call, limit) {
|
|
11
|
+
const state = manifest.classification
|
|
12
|
+
if (!state?.complete) throw new Error('CLASSIFICATION_READS_INCOMPLETE')
|
|
13
|
+
const created = []
|
|
14
|
+
let batch = []
|
|
15
|
+
const evidenceInstruction = `${classifyInstruction} This is the one bounded evidence review for each supplied family; replace each prior judgment. Structural evidence and selected excerpts are partial and do not establish privacy coverage. Return no further evidence requests. If ownership remains unclear, retain the best supported identity/work intent and state the unresolved question in groupingBasis; do not invent project identity. Final reconciliation can retain this family in Unsorted.`
|
|
16
|
+
const fits = (families) =>
|
|
17
|
+
taskTokens(directory, `evidence-${families[0].rootKey}`, evidenceInstruction, {
|
|
18
|
+
knownProjects: manifest.classification.projects.map(({ projectId, projectName }) => ({
|
|
19
|
+
projectId,
|
|
20
|
+
projectName,
|
|
21
|
+
})),
|
|
22
|
+
families,
|
|
23
|
+
}) <= 12000
|
|
24
|
+
const flush = async () => {
|
|
25
|
+
if (!batch.length) return
|
|
26
|
+
const evidenceTask = await task(
|
|
27
|
+
directory,
|
|
28
|
+
`evidence-${batch[0].rootKey}`,
|
|
29
|
+
evidenceInstruction,
|
|
30
|
+
{
|
|
31
|
+
knownProjects: manifest.classification.projects.map(({ projectId, projectName }) => ({
|
|
32
|
+
projectId,
|
|
33
|
+
projectName,
|
|
34
|
+
})),
|
|
35
|
+
families: batch,
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
const assigned = {
|
|
39
|
+
...evidenceTask,
|
|
40
|
+
rootKeys: batch.map((family) => family.rootKey),
|
|
41
|
+
evidenceRound: 1,
|
|
42
|
+
}
|
|
43
|
+
manifest.tasks.push(assigned)
|
|
44
|
+
created.push(assigned)
|
|
45
|
+
state.evidence.push(
|
|
46
|
+
...batch.map((family) => ({
|
|
47
|
+
rootKey: family.rootKey,
|
|
48
|
+
status: family.projectEvidence.status,
|
|
49
|
+
})),
|
|
50
|
+
)
|
|
51
|
+
await save(join(directory, 'manifest.json'), manifest)
|
|
52
|
+
batch = []
|
|
53
|
+
}
|
|
54
|
+
for (const item of manifest.tasks) {
|
|
55
|
+
if (item.projectKeys || item.evidenceRound > 0 || !(await optional(item.answer))) continue
|
|
56
|
+
const reply = await answers([item], 'judgments')
|
|
57
|
+
const value = JSON.parse(await readFile(item.answer, 'utf8'))
|
|
58
|
+
const requested = value.evidenceRootKeys ?? []
|
|
59
|
+
if (
|
|
60
|
+
!Array.isArray(requested) ||
|
|
61
|
+
new Set(requested).size !== requested.length ||
|
|
62
|
+
requested.some((key) => !item.rootKeys.includes(key))
|
|
63
|
+
)
|
|
64
|
+
throw new Error('INVALID_EVIDENCE_ROOTS')
|
|
65
|
+
for (const key of requested) {
|
|
66
|
+
if (state.evidence.some((entry) => entry.rootKey === key)) continue
|
|
67
|
+
if (created.length >= limit)
|
|
68
|
+
return {
|
|
69
|
+
stage: 'classification',
|
|
70
|
+
tasks: created,
|
|
71
|
+
created: created.length,
|
|
72
|
+
evidenceComplete: false,
|
|
73
|
+
}
|
|
74
|
+
const root = state.roots.find((entry) => entry.key === key)
|
|
75
|
+
const source = state.unknown.find((entry) => entry.rootKey === key)
|
|
76
|
+
if (!root || !source) throw new Error('INVALID_EVIDENCE_ROOTS')
|
|
77
|
+
const page = await call('journey.classificationSessions', {
|
|
78
|
+
journeyId: view.journeyId,
|
|
79
|
+
workId: root.workId,
|
|
80
|
+
includeProjectEvidence: true,
|
|
81
|
+
projectEvidenceRootSessionIds: [root.rootSessionId],
|
|
82
|
+
rootsOnly: true,
|
|
83
|
+
limit: 100,
|
|
84
|
+
})
|
|
85
|
+
const family = (page.sessions ?? []).filter(
|
|
86
|
+
(session) => session.rootId === root.rootSessionId,
|
|
87
|
+
)
|
|
88
|
+
if (!family.length || family.some((session) => session.rootId !== root.rootSessionId))
|
|
89
|
+
throw new Error('CLASSIFICATION_EVIDENCE_INVALID')
|
|
90
|
+
const projectEvidence = family.find(
|
|
91
|
+
(session) => session.id === root.rootSessionId,
|
|
92
|
+
)?.projectEvidence
|
|
93
|
+
if (!projectEvidence) throw new Error('CLASSIFICATION_EVIDENCE_UNAVAILABLE')
|
|
94
|
+
const rows = []
|
|
95
|
+
if (projectEvidence.status !== 'supported') {
|
|
96
|
+
let cursor
|
|
97
|
+
for (let pageNumber = 0; pageNumber < 2; pageNumber++) {
|
|
98
|
+
const content = await call('journey.classificationRead', {
|
|
99
|
+
journeyId: view.journeyId,
|
|
100
|
+
workId: root.workId,
|
|
101
|
+
rootSessionId: root.rootSessionId,
|
|
102
|
+
...(cursor ? { cursor } : {}),
|
|
103
|
+
})
|
|
104
|
+
for (const row of content.rows ?? []) {
|
|
105
|
+
if (rows.length >= 4) break
|
|
106
|
+
rows.push({ ...row, text: row.text.slice(0, 800), excerpt: row.text.length > 800 })
|
|
107
|
+
}
|
|
108
|
+
cursor = content.cursor
|
|
109
|
+
if (!cursor || rows.length >= 4) break
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const prior = reply.find((entry) => entry.rootKey === key)
|
|
113
|
+
const evidenceData = { rootKey: key, metadata: source.metadata, prior, projectEvidence, rows }
|
|
114
|
+
if (!fits([evidenceData])) throw new Error('CLASSIFICATION_EVIDENCE_TOO_LARGE')
|
|
115
|
+
if (!fits([...batch, evidenceData])) {
|
|
116
|
+
await flush()
|
|
117
|
+
if (created.length >= limit)
|
|
118
|
+
return {
|
|
119
|
+
stage: 'classification',
|
|
120
|
+
tasks: created,
|
|
121
|
+
created: created.length,
|
|
122
|
+
evidenceComplete: false,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
batch.push(evidenceData)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
await flush()
|
|
129
|
+
const pendingEvidence = []
|
|
130
|
+
for (const item of manifest.tasks) {
|
|
131
|
+
if (item.evidenceRound > 0 && !(await optional(item.answer))) pendingEvidence.push(item)
|
|
132
|
+
}
|
|
133
|
+
let allAnswered = true
|
|
134
|
+
for (const item of manifest.tasks)
|
|
135
|
+
if (!item.projectKeys && !(await optional(item.answer))) allAnswered = false
|
|
136
|
+
return {
|
|
137
|
+
stage: 'classification',
|
|
138
|
+
tasks: [
|
|
139
|
+
...created,
|
|
140
|
+
...pendingEvidence.filter((item) => !created.some((entry) => entry.id === item.id)),
|
|
141
|
+
],
|
|
142
|
+
created: created.length,
|
|
143
|
+
evidenceComplete: pendingEvidence.length === 0 && allAnswered,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { answers } from './journey-tasks.mjs'
|
|
4
|
+
import { optional } from './journey-state.mjs'
|
|
5
|
+
import {
|
|
6
|
+
validateClassificationProject,
|
|
7
|
+
validateConsolidationAnswer,
|
|
8
|
+
compileAssignments,
|
|
9
|
+
} from './journey-classification-validation.mjs'
|
|
10
|
+
|
|
11
|
+
/** Known-project Works with their fixed catalog membership and answered metadata. */
|
|
12
|
+
async function knownProjects(classification, manifest) {
|
|
13
|
+
const projectMetadata = new Map()
|
|
14
|
+
for (const item of manifest.tasks.filter((entry) => entry.projectKeys)) {
|
|
15
|
+
const results = await answers([item], 'items')
|
|
16
|
+
const ids = results.map((result) => result.projectId)
|
|
17
|
+
const expected = item.projectKeys.map(
|
|
18
|
+
(key) => classification.projects.find((project) => project.key === key)?.projectId,
|
|
19
|
+
)
|
|
20
|
+
if (
|
|
21
|
+
results.length !== expected.length ||
|
|
22
|
+
expected.some((id) => !id) ||
|
|
23
|
+
new Set(ids).size !== ids.length ||
|
|
24
|
+
expected.some((id) => !ids.includes(id))
|
|
25
|
+
)
|
|
26
|
+
throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
|
|
27
|
+
for (const result of results) {
|
|
28
|
+
validateClassificationProject(result, result.projectId)
|
|
29
|
+
if (projectMetadata.has(result.projectId))
|
|
30
|
+
throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
|
|
31
|
+
projectMetadata.set(result.projectId, result)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const result = classification.projects.map((project) => {
|
|
35
|
+
const metadata = projectMetadata.get(project.projectId)
|
|
36
|
+
if (!metadata) throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
|
|
37
|
+
return {
|
|
38
|
+
projectName: project.projectName,
|
|
39
|
+
description: metadata.description,
|
|
40
|
+
projectType: metadata.projectType,
|
|
41
|
+
rootSessionIds: project.rootKeys.map((key) => {
|
|
42
|
+
const root = classification.roots.find((entry) => entry.key === key)
|
|
43
|
+
if (!root) throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
|
|
44
|
+
return root.rootSessionId
|
|
45
|
+
}),
|
|
46
|
+
recommendation: metadata.recommendation,
|
|
47
|
+
projectId: project.projectId,
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* E2: once every known project has its metadata, its Works are published while the unresolved
|
|
55
|
+
* roots are still being grouped; they wait as `pendingRootSessionIds`. Null while metadata is
|
|
56
|
+
* missing or when nothing remains to group.
|
|
57
|
+
*/
|
|
58
|
+
export async function partialClassificationSubmission(manifest) {
|
|
59
|
+
const classification = manifest.classification
|
|
60
|
+
if (
|
|
61
|
+
!classification?.complete ||
|
|
62
|
+
!classification.unknown.length ||
|
|
63
|
+
!classification.projects.length
|
|
64
|
+
)
|
|
65
|
+
return null
|
|
66
|
+
for (const item of manifest.tasks.filter((entry) => entry.projectKeys))
|
|
67
|
+
if (!(await optional(item.answer))) return null
|
|
68
|
+
const known = await knownProjects(classification, manifest)
|
|
69
|
+
const rootByKey = new Map(classification.roots.map((root) => [root.key, root.rootSessionId]))
|
|
70
|
+
const pendingRootSessionIds = classification.unknown.map((item) => {
|
|
71
|
+
const id = rootByKey.get(item.rootKey)
|
|
72
|
+
if (!id) throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
|
|
73
|
+
return id
|
|
74
|
+
})
|
|
75
|
+
return { items: normalized(known), pendingRootSessionIds }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Expand short handles and fixed catalog membership only after exact coverage validation. */
|
|
79
|
+
export async function classificationSubmission(manifest) {
|
|
80
|
+
const classification = manifest.classification
|
|
81
|
+
if (!classification?.complete || !manifest.consolidation)
|
|
82
|
+
throw new Error('CONSOLIDATION_REQUIRED')
|
|
83
|
+
const result = await knownProjects(classification, manifest)
|
|
84
|
+
let answerText
|
|
85
|
+
let unknown
|
|
86
|
+
if (manifest.consolidation.automatic) unknown = { groups: [], assignments: {} }
|
|
87
|
+
else {
|
|
88
|
+
answerText = await readFile(manifest.consolidation.answer, 'utf8')
|
|
89
|
+
unknown = JSON.parse(answerText)
|
|
90
|
+
}
|
|
91
|
+
if (!manifest.consolidation.automatic) {
|
|
92
|
+
const digest = createHash('sha256').update(answerText).digest('hex')
|
|
93
|
+
if (!manifest.consolidation.answerHash || digest !== manifest.consolidation.answerHash)
|
|
94
|
+
throw new Error('CLASSIFICATION_CONSOLIDATION_CHANGED')
|
|
95
|
+
}
|
|
96
|
+
const candidates = manifest.consolidation.candidates ?? []
|
|
97
|
+
const rootOrder = candidates.flatMap((candidate) => candidate.roots.map((row) => row[0]))
|
|
98
|
+
const groups = unknown?.groups
|
|
99
|
+
const assignments = unknown?.assignments
|
|
100
|
+
const validation = validateConsolidationAnswer(unknown, manifest.consolidation)
|
|
101
|
+
const diagnostics = [...validation.details]
|
|
102
|
+
const expectedRootKeys = classification.unknown
|
|
103
|
+
.map((item) => item.rootKey)
|
|
104
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
105
|
+
const suppliedRootKeys = [...rootOrder].toSorted((left, right) => left.localeCompare(right))
|
|
106
|
+
if (
|
|
107
|
+
rootOrder.length !== expectedRootKeys.length ||
|
|
108
|
+
new Set(rootOrder).size !== rootOrder.length ||
|
|
109
|
+
JSON.stringify(suppliedRootKeys) !== JSON.stringify(expectedRootKeys)
|
|
110
|
+
)
|
|
111
|
+
diagnostics.push('task root order does not match current classification roots')
|
|
112
|
+
if (diagnostics.length)
|
|
113
|
+
throw Object.assign(new Error('CLASSIFICATION_CONSOLIDATION_INVALID'), {
|
|
114
|
+
code: 'CLASSIFICATION_CONSOLIDATION_INVALID',
|
|
115
|
+
details: diagnostics,
|
|
116
|
+
})
|
|
117
|
+
const groupById = new Map(groups.map((group) => [group.id, group]))
|
|
118
|
+
const expandedAssignments = compileAssignments(candidates, assignments, unknown.overrides ?? [])
|
|
119
|
+
const counts = new Map()
|
|
120
|
+
for (const id of expandedAssignments.values()) counts.set(id, (counts.get(id) ?? 0) + 1)
|
|
121
|
+
const merged = new Map()
|
|
122
|
+
const unsorted = []
|
|
123
|
+
rootOrder.forEach((key) => {
|
|
124
|
+
const group = groupById.get(expandedAssignments.get(key))
|
|
125
|
+
const useUnsorted =
|
|
126
|
+
group.groupKind === 'unsorted' ||
|
|
127
|
+
(!group.targetProjectId && group.groupKind === 'topic' && counts.get(group.id) < 3)
|
|
128
|
+
if (useUnsorted) unsorted.push(key)
|
|
129
|
+
else merged.set(group.id, [...(merged.get(group.id) ?? []), key])
|
|
130
|
+
})
|
|
131
|
+
const proposals = [...merged].map(([id, rootKeys]) => ({ ...groupById.get(id), rootKeys }))
|
|
132
|
+
if (unsorted.length)
|
|
133
|
+
proposals.push({
|
|
134
|
+
id: 'unsorted',
|
|
135
|
+
projectName: 'Unsorted',
|
|
136
|
+
groupKind: 'unsorted',
|
|
137
|
+
rootKeys: unsorted,
|
|
138
|
+
})
|
|
139
|
+
const rootByKey = new Map(classification.roots.map((root) => [root.key, root]))
|
|
140
|
+
const metadataMap = new Map()
|
|
141
|
+
for (const item of manifest.consolidation.metadataTasks ?? []) {
|
|
142
|
+
if (item.reconciliationHash !== manifest.consolidation.answerHash)
|
|
143
|
+
throw new Error('CLASSIFICATION_CONSOLIDATION_CHANGED')
|
|
144
|
+
const descriptor = JSON.parse(await readFile(item.descriptor, 'utf8'))
|
|
145
|
+
if (descriptor.reconciliationHash !== manifest.consolidation.answerHash)
|
|
146
|
+
throw new Error('CLASSIFICATION_CONSOLIDATION_CHANGED')
|
|
147
|
+
const values = await answers([item], 'items')
|
|
148
|
+
if (
|
|
149
|
+
!Array.isArray(item.projectIds) ||
|
|
150
|
+
values.length !== item.projectIds.length ||
|
|
151
|
+
new Set(item.projectIds).size !== item.projectIds.length
|
|
152
|
+
)
|
|
153
|
+
throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
|
|
154
|
+
for (const value of values) {
|
|
155
|
+
validateClassificationProject(value, value.projectId)
|
|
156
|
+
if (!item.projectIds.includes(value.projectId) || metadataMap.has(value.projectId))
|
|
157
|
+
throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
|
|
158
|
+
metadataMap.set(value.projectId, value)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const requiredMetadataIds = new Set(
|
|
162
|
+
proposals.filter((item) => !item.targetProjectId).map((item) => item.id),
|
|
163
|
+
)
|
|
164
|
+
if (
|
|
165
|
+
requiredMetadataIds.size !== metadataMap.size ||
|
|
166
|
+
[...requiredMetadataIds].some((id) => !metadataMap.has(id))
|
|
167
|
+
)
|
|
168
|
+
throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
|
|
169
|
+
for (const item of proposals) {
|
|
170
|
+
const rootSessionIds = item.rootKeys.map((key) => rootByKey.get(key)?.rootSessionId)
|
|
171
|
+
if (rootSessionIds.some((id) => !id)) throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
|
|
172
|
+
const {
|
|
173
|
+
id: _id,
|
|
174
|
+
rootKeys: _rootKeys,
|
|
175
|
+
groupKind: _groupKind,
|
|
176
|
+
targetProjectId,
|
|
177
|
+
...metadata
|
|
178
|
+
} = item
|
|
179
|
+
if (targetProjectId) {
|
|
180
|
+
const target = result.find((entry) => entry.projectId === targetProjectId)
|
|
181
|
+
if (!target) throw new Error('UNKNOWN_TARGET_PROJECT')
|
|
182
|
+
target.rootSessionIds.push(...rootSessionIds)
|
|
183
|
+
} else {
|
|
184
|
+
const submittedMetadata = metadataMap.get(item.id)
|
|
185
|
+
if (!submittedMetadata) throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
|
|
186
|
+
result.push({
|
|
187
|
+
projectName: metadata.projectName,
|
|
188
|
+
description: submittedMetadata.description,
|
|
189
|
+
projectType: submittedMetadata.projectType,
|
|
190
|
+
recommendation: submittedMetadata.recommendation,
|
|
191
|
+
rootSessionIds,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const expectedRootIds = classification.roots
|
|
196
|
+
.map((root) => root.rootSessionId)
|
|
197
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
198
|
+
const submittedRootIds = result
|
|
199
|
+
.flatMap((item) => item.rootSessionIds)
|
|
200
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
201
|
+
if (
|
|
202
|
+
new Set(submittedRootIds).size !== classification.roots.length ||
|
|
203
|
+
JSON.stringify(expectedRootIds) !== JSON.stringify(submittedRootIds)
|
|
204
|
+
)
|
|
205
|
+
throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
|
|
206
|
+
return normalized(result)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Trimmed submission items; one project name never carries two different metadata sets. */
|
|
210
|
+
function normalized(result) {
|
|
211
|
+
const normalizedNames = new Map()
|
|
212
|
+
const output = result.map(({ projectId: _projectId, ...item }) => ({
|
|
213
|
+
...item,
|
|
214
|
+
projectName: item.projectName.trim(),
|
|
215
|
+
description: item.description.trim(),
|
|
216
|
+
recommendation: { ...item.recommendation, reason: item.recommendation.reason.trim() },
|
|
217
|
+
}))
|
|
218
|
+
for (const item of output) {
|
|
219
|
+
const key = item.projectName.normalize('NFC').toLocaleLowerCase('en-US')
|
|
220
|
+
const metadata = JSON.stringify([item.description, item.projectType, item.recommendation])
|
|
221
|
+
const prior = normalizedNames.get(key)
|
|
222
|
+
if (prior !== undefined && prior !== metadata)
|
|
223
|
+
throw new Error('CLASSIFICATION_PROJECT_NAME_CONFLICT')
|
|
224
|
+
normalizedNames.set(key, metadata)
|
|
225
|
+
}
|
|
226
|
+
return output
|
|
227
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function consolidateClassification(
|
|
2
|
+
directory: string,
|
|
3
|
+
manifest: Record<string, unknown>,
|
|
4
|
+
options?: { refresh?: boolean },
|
|
5
|
+
): Promise<Record<string, unknown>>
|
|
6
|
+
|
|
7
|
+
export function classificationSubmission(
|
|
8
|
+
manifest: Record<string, unknown>,
|
|
9
|
+
): Promise<Array<Record<string, unknown>>>
|
|
10
|
+
|
|
11
|
+
export function partialClassificationSubmission(manifest: Record<string, unknown>): Promise<{
|
|
12
|
+
items: Array<Record<string, unknown>>
|
|
13
|
+
pendingRootSessionIds: string[]
|
|
14
|
+
} | null>
|