@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.
Files changed (140) hide show
  1. package/README.md +84 -2
  2. package/bin/INDEX.md +9 -0
  3. package/bin/tacitt.cjs +16 -0
  4. package/dist/agent-setup-B85GaVIz.mjs +71 -0
  5. package/dist/agent-setup-worker.mjs +56 -0
  6. package/dist/cli.mjs +1343 -0
  7. package/dist/codex-title-worker.mjs +215 -0
  8. package/dist/configured-preview-worker.mjs +133 -0
  9. package/dist/configured-session-worker.mjs +117 -0
  10. package/dist/frame-D1j9R8xX.mjs +84 -0
  11. package/dist/heap-1FYhGds4.mjs +75 -0
  12. package/dist/heap-tfU61Ehy.mjs +7253 -0
  13. package/dist/job--TceKk0l.mjs +194 -0
  14. package/dist/locate-Bn32k2se.mjs +10647 -0
  15. package/dist/model-7Nu7szNP.mjs +41 -0
  16. package/dist/operation-executor.mjs +181 -0
  17. package/dist/preview-worker.mjs +22 -0
  18. package/dist/privacy-validator.mjs +46 -0
  19. package/dist/query-Da-KDbSV.mjs +14511 -0
  20. package/dist/run-CCATsHS2.mjs +916 -0
  21. package/dist/session-process.mjs +90 -0
  22. package/dist/src-BngGpFKd.mjs +16416 -0
  23. package/dist/src-CPPjOyHZ.mjs +12782 -0
  24. package/dist/src-DynBRDBc.mjs +6590 -0
  25. package/dist/store-database-worker.mjs +14 -0
  26. package/dist/web/THIRD_PARTY_LICENSES.md +388 -0
  27. package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  28. package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  29. package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  30. package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  31. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  32. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  33. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  34. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  35. package/dist/web/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  36. package/dist/web/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  37. package/dist/web/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  38. package/dist/web/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  39. package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  40. package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  41. package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  42. package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  43. package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  44. package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  45. package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  46. package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  47. package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  48. package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  49. package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  50. package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  51. package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  52. package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  53. package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-D_uavGvn.woff2 +0 -0
  54. package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-RFb1v-CK.woff +0 -0
  55. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  56. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  57. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  58. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  59. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  60. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  61. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-BmRLXWpJ.woff2 +0 -0
  62. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-DCQwxkRk.woff +0 -0
  63. package/dist/web/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  64. package/dist/web/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  65. package/dist/web/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  66. package/dist/web/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  67. package/dist/web/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  68. package/dist/web/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  69. package/dist/web/assets/ibm-plex-sans-greek-700-normal-CkZN0sRy.woff +0 -0
  70. package/dist/web/assets/ibm-plex-sans-greek-700-normal-wyJ6jzdA.woff2 +0 -0
  71. package/dist/web/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  72. package/dist/web/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  73. package/dist/web/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  74. package/dist/web/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  75. package/dist/web/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  76. package/dist/web/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  77. package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
  78. package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
  79. package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  80. package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  81. package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  82. package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  83. package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  84. package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  85. package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-DZH6Xh-4.woff +0 -0
  86. package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-aMgXseD4.woff2 +0 -0
  87. package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  88. package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  89. package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  90. package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  91. package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  92. package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  93. package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-BV5B8Are.woff +0 -0
  94. package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-CkEo6LDy.woff2 +0 -0
  95. package/dist/web/assets/index-CWQ05PFl.js +67 -0
  96. package/dist/web/assets/index-J-AWt_cW.css +1 -0
  97. package/dist/web/index.html +14 -0
  98. package/package.json +56 -3
  99. package/skills/INDEX.md +9 -0
  100. package/skills/tacitt/INDEX.md +11 -0
  101. package/skills/tacitt/SKILL.md +42 -0
  102. package/skills/tacitt/references/INDEX.md +15 -0
  103. package/skills/tacitt/references/b-method.md +22 -0
  104. package/skills/tacitt/references/bootstrap.md +109 -0
  105. package/skills/tacitt/references/classification.md +83 -0
  106. package/skills/tacitt/references/commands.md +147 -0
  107. package/skills/tacitt/references/journey.md +52 -0
  108. package/skills/tacitt/references/parallel-review.md +64 -0
  109. package/skills/tacitt/references/send.md +30 -0
  110. package/skills/tacitt/scripts/INDEX.md +43 -0
  111. package/skills/tacitt/scripts/answer.mjs +257 -0
  112. package/skills/tacitt/scripts/describe.mjs +55 -0
  113. package/skills/tacitt/scripts/journey-card-consolidation.d.mts +8 -0
  114. package/skills/tacitt/scripts/journey-card-consolidation.mjs +146 -0
  115. package/skills/tacitt/scripts/journey-classification-metadata.d.mts +16 -0
  116. package/skills/tacitt/scripts/journey-classification-metadata.mjs +111 -0
  117. package/skills/tacitt/scripts/journey-classification-progress.mjs +194 -0
  118. package/skills/tacitt/scripts/journey-classification-tasks.d.mts +18 -0
  119. package/skills/tacitt/scripts/journey-classification-tasks.mjs +345 -0
  120. package/skills/tacitt/scripts/journey-classification-validation.d.mts +20 -0
  121. package/skills/tacitt/scripts/journey-classification-validation.mjs +219 -0
  122. package/skills/tacitt/scripts/journey-classification.mjs +145 -0
  123. package/skills/tacitt/scripts/journey-consolidation-submit.mjs +190 -0
  124. package/skills/tacitt/scripts/journey-consolidation.d.mts +9 -0
  125. package/skills/tacitt/scripts/journey-consolidation.mjs +224 -0
  126. package/skills/tacitt/scripts/journey-scan-timing.mjs +143 -0
  127. package/skills/tacitt/scripts/journey-state.d.mts +42 -0
  128. package/skills/tacitt/scripts/journey-state.mjs +268 -0
  129. package/skills/tacitt/scripts/journey-tasks.d.mts +26 -0
  130. package/skills/tacitt/scripts/journey-tasks.mjs +183 -0
  131. package/skills/tacitt/scripts/journey.d.mts +38 -0
  132. package/skills/tacitt/scripts/journey.mjs +451 -0
  133. package/skills/tacitt/scripts/local-cli.d.mts +18 -0
  134. package/skills/tacitt/scripts/local-cli.mjs +116 -0
  135. package/skills/tacitt/scripts/privacy-validator.mjs +10350 -0
  136. package/skills/tacitt/scripts/review-stream-client.d.mts +22 -0
  137. package/skills/tacitt/scripts/review-stream-client.mjs +194 -0
  138. package/skills/tacitt/scripts/review.d.mts +87 -0
  139. package/skills/tacitt/scripts/review.mjs +1589 -0
  140. package/skills/tacitt/scripts/verify-cli.mjs +10 -0
@@ -0,0 +1,219 @@
1
+ const types = ['work', 'side', 'coursework', 'personal', 'scratch', 'other']
2
+ const access = ['public', 'private', 'drop']
3
+ const text = (value, max) =>
4
+ typeof value === 'string' && value.trim().length > 0 && value.length <= max
5
+
6
+ export function validateClassificationJudgment(value, key) {
7
+ if (
8
+ !value ||
9
+ value.rootKey !== key ||
10
+ !text(value.projectName, 240) ||
11
+ !text(value.workIntent, 240) ||
12
+ !text(value.groupingBasis, 200) ||
13
+ (value.targetProjectId !== undefined &&
14
+ (typeof value.targetProjectId !== 'string' || !value.targetProjectId)) ||
15
+ Object.keys(value).some(
16
+ (field) =>
17
+ !['rootKey', 'projectName', 'workIntent', 'groupingBasis', 'targetProjectId'].includes(
18
+ field,
19
+ ),
20
+ )
21
+ )
22
+ throw new Error('CLASSIFICATION_JUDGMENT_INVALID')
23
+ }
24
+
25
+ export function validateClassificationProject(value, projectId) {
26
+ if (
27
+ !value ||
28
+ value.projectId !== projectId ||
29
+ !text(value.description, 1000) ||
30
+ !types.includes(value.projectType) ||
31
+ !access.includes(value.recommendation?.access) ||
32
+ !text(value.recommendation?.reason, 200) ||
33
+ Object.keys(value).some(
34
+ (field) => !['projectId', 'description', 'projectType', 'recommendation'].includes(field),
35
+ )
36
+ )
37
+ throw new Error('PROJECT_METADATA_INVALID')
38
+ }
39
+
40
+ export function validateConsolidationAnswer(answer, descriptor) {
41
+ const details = []
42
+ const groups = answer?.groups
43
+ const assignments = answer?.assignments
44
+ const candidates = descriptor?.candidates
45
+ const candidateKeys = new Set()
46
+ const descriptorRoots = new Set()
47
+ if (!Array.isArray(candidates)) details.push('descriptor.candidates is missing')
48
+ else
49
+ candidates.forEach((candidate, index) => {
50
+ if (
51
+ !candidate ||
52
+ typeof candidate.key !== 'string' ||
53
+ !candidate.key ||
54
+ typeof candidate.projectName !== 'string' ||
55
+ !Array.isArray(candidate.roots) ||
56
+ candidate.roots.some(
57
+ (row) =>
58
+ !Array.isArray(row) ||
59
+ (row.length !== 3 && row.length !== 4) ||
60
+ typeof row[0] !== 'string' ||
61
+ (row.length === 4 && (typeof row[3] !== 'string' || !row[3])),
62
+ )
63
+ )
64
+ details.push(`descriptor.candidates[${index}] is invalid`)
65
+ else {
66
+ if (candidateKeys.has(candidate.key))
67
+ details.push(`descriptor.candidates[${index}].key is duplicated`)
68
+ candidateKeys.add(candidate.key)
69
+ for (const row of candidate.roots) {
70
+ if (descriptorRoots.has(row[0]))
71
+ details.push(`descriptor.candidates[${index}] repeats a root`)
72
+ descriptorRoots.add(row[0])
73
+ }
74
+ }
75
+ })
76
+ const rootOrder = Array.isArray(candidates)
77
+ ? candidates.flatMap((candidate) =>
78
+ Array.isArray(candidate?.roots)
79
+ ? candidate.roots.map((row) => (Array.isArray(row) ? row[0] : null))
80
+ : [],
81
+ )
82
+ : null
83
+ const catalog = descriptor?.projectCatalog
84
+ if (!Array.isArray(groups)) details.push('groups must be an array')
85
+ if (!assignments || typeof assignments !== 'object' || Array.isArray(assignments))
86
+ details.push('assignments must be an object keyed by candidate key')
87
+ if (!Array.isArray(catalog)) details.push('descriptor.projectCatalog is missing')
88
+ if (Array.isArray(catalog) && Array.isArray(candidates))
89
+ for (const candidate of candidates)
90
+ for (const row of candidate.roots ?? [])
91
+ if (row.length === 4 && !catalog.some((entry) => entry.projectId === row[3]))
92
+ details.push(`root ${row[0]} has an unknown targetProjectId`)
93
+ if (assignments && typeof assignments === 'object' && !Array.isArray(assignments)) {
94
+ for (const key of candidateKeys)
95
+ if (!Object.hasOwn(assignments, key))
96
+ details.push(`assignments is missing candidate key ${key}`)
97
+ for (const key of Object.keys(assignments))
98
+ if (!candidateKeys.has(key)) details.push(`assignments contains unknown candidate key ${key}`)
99
+ }
100
+ const ids = new Set()
101
+ const groupIndexes = new Map()
102
+ const names = new Map()
103
+ if (Array.isArray(groups))
104
+ groups.forEach((group, index) => {
105
+ if (!group || typeof group.id !== 'string' || !group.id.trim()) {
106
+ details.push(`groups[${index}].id must be a non-empty string`)
107
+ return
108
+ }
109
+ if (ids.has(group.id))
110
+ details.push(`groups[${index}].id duplicates groups[${groupIndexes.get(group.id)}].id`)
111
+ if (
112
+ Object.keys(group).some(
113
+ (field) => !['id', 'projectName', 'groupKind', 'targetProjectId'].includes(field),
114
+ )
115
+ )
116
+ details.push(`groups[${index}] contains unsupported fields`)
117
+ if (
118
+ group.targetProjectId !== undefined &&
119
+ (typeof group.targetProjectId !== 'string' || !group.targetProjectId)
120
+ )
121
+ details.push(`groups[${index}].targetProjectId is invalid`)
122
+ ids.add(group.id)
123
+ groupIndexes.set(group.id, index)
124
+ if (!['project', 'topic', 'unsorted'].includes(group.groupKind))
125
+ details.push(`groups[${index}].groupKind is invalid`)
126
+ if (!text(group.projectName, 240)) details.push(`groups[${index}].projectName is invalid`)
127
+ if (
128
+ group.targetProjectId &&
129
+ Array.isArray(catalog) &&
130
+ !catalog.some((entry) => entry.projectId === group.targetProjectId)
131
+ )
132
+ details.push(`groups[${index}].targetProjectId is unknown`)
133
+ if (group.targetProjectId && group.groupKind !== 'project')
134
+ details.push(`groups[${index}].targetProjectId requires groupKind project`)
135
+ if (
136
+ group.groupKind !== 'unsorted' &&
137
+ typeof group.projectName === 'string' &&
138
+ group.projectName.trim()
139
+ ) {
140
+ const name = group.projectName.normalize('NFC').toLocaleLowerCase('en-US')
141
+ if (
142
+ Array.isArray(catalog) &&
143
+ catalog.some(
144
+ (entry) => entry.projectName.normalize('NFC').toLocaleLowerCase('en-US') === name,
145
+ ) &&
146
+ !group.targetProjectId
147
+ )
148
+ details.push(
149
+ `groups[${index}].projectName conflicts with a known project; targetProjectId is required`,
150
+ )
151
+ const identity = JSON.stringify([group.targetProjectId ?? null, name])
152
+ const prior = names.get(identity)
153
+ if (prior)
154
+ details.push(`groups[${index}].projectName duplicates groups[${prior.index}].projectName`)
155
+ else names.set(identity, { index, targetProjectId: group.targetProjectId })
156
+ }
157
+ })
158
+ if (assignments && typeof assignments === 'object' && !Array.isArray(assignments))
159
+ for (const [key, id] of Object.entries(assignments))
160
+ if (typeof id !== 'string' || !ids.has(id))
161
+ details.push(`assignments[${key}] names an unknown group id`)
162
+ const roots = new Set(rootOrder ?? [])
163
+ const overrides = answer?.overrides ?? []
164
+ if (!Array.isArray(overrides)) details.push('overrides must be an array when present')
165
+ const overridden = new Set()
166
+ if (Array.isArray(overrides))
167
+ overrides.forEach((override, index) => {
168
+ if (!override || typeof override.rootKey !== 'string' || !roots.has(override.rootKey))
169
+ details.push(`overrides[${index}].rootKey is unknown`)
170
+ else if (overridden.has(override.rootKey))
171
+ details.push(`overrides[${index}].rootKey is duplicated`)
172
+ else overridden.add(override.rootKey)
173
+ if (!override || typeof override.groupId !== 'string' || !ids.has(override.groupId))
174
+ details.push(`overrides[${index}].groupId names an unknown group id`)
175
+ })
176
+ if (
177
+ Array.isArray(candidates) &&
178
+ assignments &&
179
+ typeof assignments === 'object' &&
180
+ !Array.isArray(assignments) &&
181
+ Array.isArray(overrides) &&
182
+ candidates.every((candidate) => Array.isArray(candidate?.roots))
183
+ ) {
184
+ const finalIds = new Map()
185
+ const requiredTargets = new Map()
186
+ candidates.forEach((candidate) => {
187
+ for (const row of candidate.roots) {
188
+ finalIds.set(
189
+ row[0],
190
+ Object.hasOwn(assignments, candidate.key) ? assignments[candidate.key] : null,
191
+ )
192
+ if (row[3]) requiredTargets.set(row[0], row[3])
193
+ }
194
+ })
195
+ for (const override of overrides)
196
+ if (typeof override?.rootKey === 'string' && typeof override?.groupId === 'string')
197
+ finalIds.set(override.rootKey, override.groupId)
198
+ for (const [rootKey, targetProjectId] of requiredTargets) {
199
+ const group = Array.isArray(groups)
200
+ ? groups.find((entry) => entry.id === finalIds.get(rootKey))
201
+ : null
202
+ if (group && group.targetProjectId !== targetProjectId)
203
+ details.push(`root ${rootKey} must remain assigned to targetProjectId ${targetProjectId}`)
204
+ }
205
+ const populated = new Set(finalIds.values())
206
+ for (const [id, index] of groupIndexes)
207
+ if (!populated.has(id)) details.push(`groups[${index}] has no roots after overrides`)
208
+ }
209
+ return { code: details.length ? 'CLASSIFICATION_CONSOLIDATION_INVALID' : null, details }
210
+ }
211
+
212
+ export function compileAssignments(candidates, assignments, overrides = []) {
213
+ const result = new Map()
214
+ for (const candidate of candidates)
215
+ for (const row of candidate.roots)
216
+ if (Object.hasOwn(assignments, candidate.key)) result.set(row[0], assignments[candidate.key])
217
+ for (const override of overrides) result.set(override.rootKey, override.groupId)
218
+ return result
219
+ }
@@ -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,190 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { createHash } from 'node:crypto'
3
+ import { answers } from './journey-tasks.mjs'
4
+ import {
5
+ validateClassificationProject,
6
+ validateConsolidationAnswer,
7
+ compileAssignments,
8
+ } from './journey-classification-validation.mjs'
9
+
10
+ /** Expand short handles and fixed catalog membership only after exact coverage validation. */
11
+ export async function classificationSubmission(manifest) {
12
+ const classification = manifest.classification
13
+ if (!classification?.complete || !manifest.consolidation)
14
+ throw new Error('CONSOLIDATION_REQUIRED')
15
+ const projectMetadata = new Map()
16
+ for (const item of manifest.tasks.filter((entry) => entry.projectKeys)) {
17
+ const results = await answers([item], 'items')
18
+ const ids = results.map((result) => result.projectId)
19
+ const expected = item.projectKeys.map(
20
+ (key) => classification.projects.find((project) => project.key === key)?.projectId,
21
+ )
22
+ if (
23
+ results.length !== expected.length ||
24
+ expected.some((id) => !id) ||
25
+ new Set(ids).size !== ids.length ||
26
+ expected.some((id) => !ids.includes(id))
27
+ )
28
+ throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
29
+ for (const result of results) {
30
+ validateClassificationProject(result, result.projectId)
31
+ if (projectMetadata.has(result.projectId))
32
+ throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
33
+ projectMetadata.set(result.projectId, result)
34
+ }
35
+ }
36
+ const result = classification.projects.map((project) => {
37
+ const metadata = projectMetadata.get(project.projectId)
38
+ if (!metadata) throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
39
+ return {
40
+ projectName: project.projectName,
41
+ description: metadata.description,
42
+ projectType: metadata.projectType,
43
+ rootSessionIds: project.rootKeys.map((key) => {
44
+ const root = classification.roots.find((entry) => entry.key === key)
45
+ if (!root) throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
46
+ return root.rootSessionId
47
+ }),
48
+ recommendation: metadata.recommendation,
49
+ projectId: project.projectId,
50
+ }
51
+ })
52
+ let answerText
53
+ let unknown
54
+ if (manifest.consolidation.automatic) unknown = { groups: [], assignments: {} }
55
+ else {
56
+ answerText = await readFile(manifest.consolidation.answer, 'utf8')
57
+ unknown = JSON.parse(answerText)
58
+ }
59
+ if (!manifest.consolidation.automatic) {
60
+ const digest = createHash('sha256').update(answerText).digest('hex')
61
+ if (!manifest.consolidation.answerHash || digest !== manifest.consolidation.answerHash)
62
+ throw new Error('CLASSIFICATION_CONSOLIDATION_CHANGED')
63
+ }
64
+ const candidates = manifest.consolidation.candidates ?? []
65
+ const rootOrder = candidates.flatMap((candidate) => candidate.roots.map((row) => row[0]))
66
+ const groups = unknown?.groups
67
+ const assignments = unknown?.assignments
68
+ const validation = validateConsolidationAnswer(unknown, manifest.consolidation)
69
+ const diagnostics = [...validation.details]
70
+ const expectedRootKeys = classification.unknown
71
+ .map((item) => item.rootKey)
72
+ .toSorted((left, right) => left.localeCompare(right))
73
+ const suppliedRootKeys = [...rootOrder].toSorted((left, right) => left.localeCompare(right))
74
+ if (
75
+ rootOrder.length !== expectedRootKeys.length ||
76
+ new Set(rootOrder).size !== rootOrder.length ||
77
+ JSON.stringify(suppliedRootKeys) !== JSON.stringify(expectedRootKeys)
78
+ )
79
+ diagnostics.push('task root order does not match current classification roots')
80
+ if (diagnostics.length)
81
+ throw Object.assign(new Error('CLASSIFICATION_CONSOLIDATION_INVALID'), {
82
+ code: 'CLASSIFICATION_CONSOLIDATION_INVALID',
83
+ details: diagnostics,
84
+ })
85
+ const groupById = new Map(groups.map((group) => [group.id, group]))
86
+ const expandedAssignments = compileAssignments(candidates, assignments, unknown.overrides ?? [])
87
+ const counts = new Map()
88
+ for (const id of expandedAssignments.values()) counts.set(id, (counts.get(id) ?? 0) + 1)
89
+ const merged = new Map()
90
+ const unsorted = []
91
+ rootOrder.forEach((key) => {
92
+ const group = groupById.get(expandedAssignments.get(key))
93
+ const useUnsorted =
94
+ group.groupKind === 'unsorted' ||
95
+ (!group.targetProjectId && group.groupKind === 'topic' && counts.get(group.id) < 3)
96
+ if (useUnsorted) unsorted.push(key)
97
+ else merged.set(group.id, [...(merged.get(group.id) ?? []), key])
98
+ })
99
+ const proposals = [...merged].map(([id, rootKeys]) => ({ ...groupById.get(id), rootKeys }))
100
+ if (unsorted.length)
101
+ proposals.push({
102
+ id: 'unsorted',
103
+ projectName: 'Unsorted',
104
+ groupKind: 'unsorted',
105
+ rootKeys: unsorted,
106
+ })
107
+ const rootByKey = new Map(classification.roots.map((root) => [root.key, root]))
108
+ const metadataMap = new Map()
109
+ for (const item of manifest.consolidation.metadataTasks ?? []) {
110
+ if (item.reconciliationHash !== manifest.consolidation.answerHash)
111
+ throw new Error('CLASSIFICATION_CONSOLIDATION_CHANGED')
112
+ const descriptor = JSON.parse(await readFile(item.descriptor, 'utf8'))
113
+ if (descriptor.reconciliationHash !== manifest.consolidation.answerHash)
114
+ throw new Error('CLASSIFICATION_CONSOLIDATION_CHANGED')
115
+ const values = await answers([item], 'items')
116
+ if (
117
+ !Array.isArray(item.projectIds) ||
118
+ values.length !== item.projectIds.length ||
119
+ new Set(item.projectIds).size !== item.projectIds.length
120
+ )
121
+ throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
122
+ for (const value of values) {
123
+ validateClassificationProject(value, value.projectId)
124
+ if (!item.projectIds.includes(value.projectId) || metadataMap.has(value.projectId))
125
+ throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
126
+ metadataMap.set(value.projectId, value)
127
+ }
128
+ }
129
+ const requiredMetadataIds = new Set(
130
+ proposals.filter((item) => !item.targetProjectId).map((item) => item.id),
131
+ )
132
+ if (
133
+ requiredMetadataIds.size !== metadataMap.size ||
134
+ [...requiredMetadataIds].some((id) => !metadataMap.has(id))
135
+ )
136
+ throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
137
+ for (const item of proposals) {
138
+ const rootSessionIds = item.rootKeys.map((key) => rootByKey.get(key)?.rootSessionId)
139
+ if (rootSessionIds.some((id) => !id)) throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
140
+ const {
141
+ id: _id,
142
+ rootKeys: _rootKeys,
143
+ groupKind: _groupKind,
144
+ targetProjectId,
145
+ ...metadata
146
+ } = item
147
+ if (targetProjectId) {
148
+ const target = result.find((entry) => entry.projectId === targetProjectId)
149
+ if (!target) throw new Error('UNKNOWN_TARGET_PROJECT')
150
+ target.rootSessionIds.push(...rootSessionIds)
151
+ } else {
152
+ const submittedMetadata = metadataMap.get(item.id)
153
+ if (!submittedMetadata) throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
154
+ result.push({
155
+ projectName: metadata.projectName,
156
+ description: submittedMetadata.description,
157
+ projectType: submittedMetadata.projectType,
158
+ recommendation: submittedMetadata.recommendation,
159
+ rootSessionIds,
160
+ })
161
+ }
162
+ }
163
+ const expectedRootIds = classification.roots
164
+ .map((root) => root.rootSessionId)
165
+ .toSorted((left, right) => left.localeCompare(right))
166
+ const submittedRootIds = result
167
+ .flatMap((item) => item.rootSessionIds)
168
+ .toSorted((left, right) => left.localeCompare(right))
169
+ if (
170
+ new Set(submittedRootIds).size !== classification.roots.length ||
171
+ JSON.stringify(expectedRootIds) !== JSON.stringify(submittedRootIds)
172
+ )
173
+ throw new Error('CLASSIFICATION_ROOT_COVERAGE_INVALID')
174
+ const normalizedNames = new Map()
175
+ const output = result.map(({ projectId: _projectId, ...item }) => ({
176
+ ...item,
177
+ projectName: item.projectName.trim(),
178
+ description: item.description.trim(),
179
+ recommendation: { ...item.recommendation, reason: item.recommendation.reason.trim() },
180
+ }))
181
+ for (const item of output) {
182
+ const key = item.projectName.normalize('NFC').toLocaleLowerCase('en-US')
183
+ const metadata = JSON.stringify([item.description, item.projectType, item.recommendation])
184
+ const prior = normalizedNames.get(key)
185
+ if (prior !== undefined && prior !== metadata)
186
+ throw new Error('CLASSIFICATION_PROJECT_NAME_CONFLICT')
187
+ normalizedNames.set(key, metadata)
188
+ }
189
+ return output
190
+ }
@@ -0,0 +1,9 @@
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>>>