@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.
Files changed (142) 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-B8DAiQ6Z.mjs +71 -0
  5. package/dist/agent-setup-worker.mjs +56 -0
  6. package/dist/cli.mjs +1442 -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-waS5FnN5.mjs +84 -0
  11. package/dist/heap-1FYhGds4.mjs +75 -0
  12. package/dist/heap-BHcvxWCw.mjs +7265 -0
  13. package/dist/inspection-worker.mjs +57 -0
  14. package/dist/job-J3NFw3MH.mjs +194 -0
  15. package/dist/locate-U3-nkird.mjs +10670 -0
  16. package/dist/messages-D3m3bhva.mjs +40 -0
  17. package/dist/model-BJdTLNjT.mjs +41 -0
  18. package/dist/operation-executor.mjs +181 -0
  19. package/dist/preview-worker.mjs +22 -0
  20. package/dist/privacy-validator.mjs +46 -0
  21. package/dist/query-BA0XCwnR.mjs +14869 -0
  22. package/dist/run-BeLHsZWb.mjs +916 -0
  23. package/dist/session-process.mjs +90 -0
  24. package/dist/src-B5SArpVY.mjs +6601 -0
  25. package/dist/src-CC95LYYp.mjs +12985 -0
  26. package/dist/src-k0Ya1Bri.mjs +16422 -0
  27. package/dist/store-database-worker.mjs +14 -0
  28. package/dist/web/THIRD_PARTY_LICENSES.md +388 -0
  29. package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  30. package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  31. package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  32. package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  33. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  34. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  35. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  36. package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  37. package/dist/web/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  38. package/dist/web/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  39. package/dist/web/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  40. package/dist/web/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  41. package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  42. package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  43. package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  44. package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  45. package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  46. package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  47. package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  48. package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  49. package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  50. package/dist/web/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  51. package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  52. package/dist/web/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  53. package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  54. package/dist/web/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  55. package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-D_uavGvn.woff2 +0 -0
  56. package/dist/web/assets/ibm-plex-sans-cyrillic-700-normal-RFb1v-CK.woff +0 -0
  57. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  58. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  59. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  60. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  61. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  62. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  63. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-BmRLXWpJ.woff2 +0 -0
  64. package/dist/web/assets/ibm-plex-sans-cyrillic-ext-700-normal-DCQwxkRk.woff +0 -0
  65. package/dist/web/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  66. package/dist/web/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  67. package/dist/web/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  68. package/dist/web/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  69. package/dist/web/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  70. package/dist/web/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  71. package/dist/web/assets/ibm-plex-sans-greek-700-normal-CkZN0sRy.woff +0 -0
  72. package/dist/web/assets/ibm-plex-sans-greek-700-normal-wyJ6jzdA.woff2 +0 -0
  73. package/dist/web/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  74. package/dist/web/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  75. package/dist/web/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  76. package/dist/web/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  77. package/dist/web/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  78. package/dist/web/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  79. package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
  80. package/dist/web/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
  81. package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  82. package/dist/web/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  83. package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  84. package/dist/web/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  85. package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  86. package/dist/web/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  87. package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-DZH6Xh-4.woff +0 -0
  88. package/dist/web/assets/ibm-plex-sans-latin-ext-700-normal-aMgXseD4.woff2 +0 -0
  89. package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  90. package/dist/web/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  91. package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  92. package/dist/web/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  93. package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  94. package/dist/web/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  95. package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-BV5B8Are.woff +0 -0
  96. package/dist/web/assets/ibm-plex-sans-vietnamese-700-normal-CkEo6LDy.woff2 +0 -0
  97. package/dist/web/assets/index-Bjos73wK.js +67 -0
  98. package/dist/web/assets/index-Bl4C2b2_.css +1 -0
  99. package/dist/web/index.html +14 -0
  100. package/package.json +56 -3
  101. package/skills/INDEX.md +9 -0
  102. package/skills/tacitt/INDEX.md +11 -0
  103. package/skills/tacitt/SKILL.md +44 -0
  104. package/skills/tacitt/references/INDEX.md +15 -0
  105. package/skills/tacitt/references/b-method.md +22 -0
  106. package/skills/tacitt/references/bootstrap.md +109 -0
  107. package/skills/tacitt/references/classification.md +83 -0
  108. package/skills/tacitt/references/commands.md +147 -0
  109. package/skills/tacitt/references/journey.md +54 -0
  110. package/skills/tacitt/references/parallel-review.md +64 -0
  111. package/skills/tacitt/references/send.md +30 -0
  112. package/skills/tacitt/scripts/INDEX.md +43 -0
  113. package/skills/tacitt/scripts/answer.mjs +278 -0
  114. package/skills/tacitt/scripts/describe.mjs +55 -0
  115. package/skills/tacitt/scripts/journey-card-consolidation.d.mts +17 -0
  116. package/skills/tacitt/scripts/journey-card-consolidation.mjs +209 -0
  117. package/skills/tacitt/scripts/journey-classification-metadata.d.mts +16 -0
  118. package/skills/tacitt/scripts/journey-classification-metadata.mjs +111 -0
  119. package/skills/tacitt/scripts/journey-classification-progress.mjs +195 -0
  120. package/skills/tacitt/scripts/journey-classification-tasks.d.mts +18 -0
  121. package/skills/tacitt/scripts/journey-classification-tasks.mjs +345 -0
  122. package/skills/tacitt/scripts/journey-classification-validation.d.mts +20 -0
  123. package/skills/tacitt/scripts/journey-classification-validation.mjs +240 -0
  124. package/skills/tacitt/scripts/journey-classification.mjs +145 -0
  125. package/skills/tacitt/scripts/journey-consolidation-submit.mjs +227 -0
  126. package/skills/tacitt/scripts/journey-consolidation.d.mts +14 -0
  127. package/skills/tacitt/scripts/journey-consolidation.mjs +227 -0
  128. package/skills/tacitt/scripts/journey-scan-timing.mjs +143 -0
  129. package/skills/tacitt/scripts/journey-state.d.mts +43 -0
  130. package/skills/tacitt/scripts/journey-state.mjs +294 -0
  131. package/skills/tacitt/scripts/journey-tasks.d.mts +26 -0
  132. package/skills/tacitt/scripts/journey-tasks.mjs +185 -0
  133. package/skills/tacitt/scripts/journey.d.mts +40 -0
  134. package/skills/tacitt/scripts/journey.mjs +592 -0
  135. package/skills/tacitt/scripts/local-cli.d.mts +18 -0
  136. package/skills/tacitt/scripts/local-cli.mjs +116 -0
  137. package/skills/tacitt/scripts/privacy-validator.mjs +10369 -0
  138. package/skills/tacitt/scripts/review-stream-client.d.mts +22 -0
  139. package/skills/tacitt/scripts/review-stream-client.mjs +194 -0
  140. package/skills/tacitt/scripts/review.d.mts +87 -0
  141. package/skills/tacitt/scripts/review.mjs +1610 -0
  142. 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>