@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,227 @@
1
+ import { join } from 'node:path'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { randomUUID } from 'node:crypto'
4
+ import { optional, save } from './journey-state.mjs'
5
+ import { answers, task, taskTokens } from './journey-tasks.mjs'
6
+ import {
7
+ validateClassificationJudgment,
8
+ validateClassificationProject,
9
+ } from './journey-classification-validation.mjs'
10
+ import { createMetadataTasks } from './journey-classification-metadata.mjs'
11
+
12
+ const budget = 90000
13
+ const instruction =
14
+ 'You own the semantic audit and reconciliation of every supplied candidate/root; Main only dispatches this generated task and runs CLI validation/ingestion. Read every task range, including all root intents, evidence and context, then author and publish the complete answer yourself. Reconcile candidate buckets globally into final Works, not one final group per input bucket. Input size is not output size: provide only a compact group catalog and one explicit candidate-key-to-group-ID assignment for every candidate. Before proposing a new project, topic or Unsorted, compare every candidate/root against every entry in catalog. Merge equivalent supported identities across differently named candidates and attach proven existing projects using the exact catalog targetProjectId. A root with a targetProjectId has a fixed identity constraint: it must finish in a group with that same targetProjectId, including after overrides; do not redirect it or erase that target. Other roots may be attached to its group when supported. Assign miscellaneous candidates to one Unsorted; retain a topic only when at least three roots genuinely share it. Return ONLY {groups:[{id,projectName,groupKind,targetProjectId?}],assignments:{candidateKey:groupId,...},overrides?:[{rootKey,groupId}]}. Assignment keys must be the exact supplied candidate.key strings; include each exactly once and no other keys. Do not use positional arrays. Do not include description, projectType, recommendation, privacy rationale or other fields. An assignment initially applies to every root in that candidate; overrides split homonyms or path conflicts but cannot violate an existing targetProjectId constraint. Do not merge identities merely because names match. Every supplied root must end in exactly one group. Do not include root keys in groups or invent identities. Known projects attach with exact targetProjectId. The rootContext rows include CLI facts and bounded partial projectEvidence. Partial references are positive evidence, not completeness proof; missing evidence is not negative proof. Checking, using, connecting to, or integrating with a product/tool does not prove that a root develops that product’s repository; require independent repository or content evidence before assigning product project identity. A product topic may remain when roots genuinely share it. A scratch workspace name alone is not project evidence, even if descriptive; however do not ignore exact repository references or independent content evidence. Reject unsupported project or topic claims in workIntent or groupingBasis that merely reinterpret a generated scratch/temp name or copy a sibling’s activity. Missing root-specific intent must remain unknown; a coherent topic requires positive evidence for that root’s membership, otherwise use Unsorted. Do not turn an uncertain initial guess into evidence by repeating it. Before publishing, audit every candidate with workspaceKind folder separately from scratch/temp: a supported named ordinary-folder project is groupKind project, including a singleton; lack of Git metadata does not downgrade it to topic or Unsorted. The known catalog is not exhaustive. Before assigning Unsorted, audit every root with repository references, changes, or an ordinary named folder, together with its sibling roots: an independently evidenced new repository can form its own project even with one root. Missing catalog membership is not negative evidence. Before publishing, compare every retained new group with known project aliases and product-specific task intent so connection checks and supporting work are not split from their supported parent project. Do not use repository reference counts, change counts or apparent dominance as a majority vote for genuinely cross-project roots. Ambiguous or incomplete cross-project evidence needs direct owning intent before adding a targetProjectId; otherwise preserve uncertainty and leave the root unbound. Existing fixed targets remain binding; do not invent corroboration for them. Keep clear projects independent. CLI combines topics with fewer than three final roots and miscellaneous roots into Unsorted. Preserve clear projects with one root. Do not omit roots or force unrelated roots into themes. Names <=240 chars. This is a proposal, never approval.'
15
+
16
+ function dispatchDescriptor(item) {
17
+ return {
18
+ id: item.id,
19
+ path: item.path,
20
+ answer: item.answer,
21
+ descriptor: item.descriptor,
22
+ prompt: item.prompt,
23
+ inputHash: item.inputHash,
24
+ }
25
+ }
26
+
27
+ const formatPayload = (data) => {
28
+ const candidates = data.candidates.map(
29
+ (candidate) =>
30
+ `{${Object.entries(candidate)
31
+ .map(
32
+ ([key, value]) =>
33
+ `${JSON.stringify(key)}:${key === 'roots' ? `[\n${value.map((row) => JSON.stringify(row)).join(',\n')}\n]` : JSON.stringify(value)}`,
34
+ )
35
+ .join(',')}}`,
36
+ )
37
+ return `{"catalog":${JSON.stringify(data.catalog)},"rootContext":[\n${data.rootContext.map((row) => JSON.stringify(row)).join(',\n')}\n],"candidates":[\n${candidates.join(',\n')}\n]}`
38
+ }
39
+
40
+ async function latestJudgments(manifest) {
41
+ const latest = new Map()
42
+ const pendingProjectTasks = []
43
+ for (const item of manifest.tasks) {
44
+ if (item.projectKeys) {
45
+ if (!(await optional(item.answer))) {
46
+ pendingProjectTasks.push(item)
47
+ continue
48
+ }
49
+ const results = await answers([item], 'items')
50
+ const ids = results.map((result) => result.projectId)
51
+ if (ids.length !== item.projectKeys.length || new Set(ids).size !== ids.length)
52
+ throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
53
+ const expected = item.projectKeys.map(
54
+ (key) => manifest.classification.projects.find((project) => project.key === key)?.projectId,
55
+ )
56
+ if (expected.some((id) => !id) || expected.some((id) => !ids.includes(id)))
57
+ throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
58
+ for (const result of results) {
59
+ if (
60
+ !item.projectKeys.includes(
61
+ manifest.classification.projects.find(
62
+ (project) => project.projectId === result.projectId,
63
+ )?.key,
64
+ )
65
+ )
66
+ throw new Error('PROJECT_METADATA_COVERAGE_INVALID')
67
+ validateClassificationProject(result, result.projectId)
68
+ }
69
+ continue
70
+ }
71
+ if (!(await optional(item.answer))) continue
72
+ const results = await answers([item], 'judgments')
73
+ const value = JSON.parse(await readFile(item.answer, 'utf8'))
74
+ const requested = value.evidenceRootKeys ?? []
75
+ if (!Array.isArray(requested) || requested.some((key) => !item.rootKeys.includes(key)))
76
+ throw new Error('INVALID_EVIDENCE_ROOTS')
77
+ if (item.evidenceRound > 0 && requested.length) throw new Error('EVIDENCE_ESCALATION_LIMIT')
78
+ const seen = new Set()
79
+ for (const result of results) {
80
+ if (!item.rootKeys.includes(result.rootKey) || seen.has(result.rootKey))
81
+ throw new Error('CLASSIFICATION_JUDGMENT_COVERAGE_INVALID')
82
+ validateClassificationJudgment(result, result.rootKey)
83
+ if (
84
+ result.targetProjectId &&
85
+ !manifest.classification.projects.some(
86
+ (project) => project.projectId === result.targetProjectId,
87
+ )
88
+ )
89
+ throw new Error('UNKNOWN_TARGET_PROJECT')
90
+ seen.add(result.rootKey)
91
+ latest.set(result.rootKey, result)
92
+ }
93
+ if (seen.size !== item.rootKeys.length)
94
+ throw new Error('CLASSIFICATION_JUDGMENT_COVERAGE_INVALID')
95
+ }
96
+ return { latest, pendingProjectTasks }
97
+ }
98
+
99
+ export async function consolidateClassification(directory, manifest, { refresh = false } = {}) {
100
+ if (!manifest.classification?.complete) throw new Error('CLASSIFICATION_READS_INCOMPLETE')
101
+ const { latest, pendingProjectTasks } = await latestJudgments(manifest)
102
+ const unknownKeys = manifest.classification.unknown.map((item) => item.rootKey)
103
+ if (unknownKeys.some((key) => !latest.has(key)))
104
+ throw new Error('CLASSIFICATION_ANSWERS_INCOMPLETE')
105
+ if (refresh && (!manifest.consolidation || manifest.consolidation.automatic))
106
+ throw new Error('CLASSIFICATION_REFRESH_UNAVAILABLE')
107
+ if (unknownKeys.length === 0) {
108
+ manifest.consolidation = {
109
+ automatic: true,
110
+ answer: null,
111
+ candidates: [],
112
+ projectCatalog: manifest.classification.projects.map((project) => ({
113
+ projectId: project.projectId,
114
+ projectName: project.projectName,
115
+ })),
116
+ }
117
+ await save(join(directory, 'manifest.json'), manifest)
118
+ return {
119
+ stage: 'classification',
120
+ consolidationReady: true,
121
+ readyForIngest: pendingProjectTasks.length === 0,
122
+ automatic: true,
123
+ tasks: pendingProjectTasks,
124
+ }
125
+ }
126
+ if (manifest.consolidation) {
127
+ if (refresh && manifest.consolidation.automatic)
128
+ throw new Error('CLASSIFICATION_REFRESH_UNAVAILABLE')
129
+ if (refresh) {
130
+ manifest.consolidationHistory ??= []
131
+ manifest.consolidationHistory.push({
132
+ ...manifest.consolidation,
133
+ supersededAt: new Date().toISOString(),
134
+ })
135
+ } else {
136
+ if (manifest.consolidation.automatic)
137
+ return {
138
+ stage: 'classification',
139
+ consolidationReady: true,
140
+ readyForIngest: pendingProjectTasks.length === 0,
141
+ automatic: true,
142
+ tasks: pendingProjectTasks,
143
+ }
144
+ if (!(await optional(manifest.consolidation.answer)))
145
+ return {
146
+ stage: 'classification',
147
+ consolidationReady: true,
148
+ readyForIngest: false,
149
+ tasks: [dispatchDescriptor(manifest.consolidation)],
150
+ }
151
+ const result = await createMetadataTasks(directory, manifest)
152
+ const tasks = [...pendingProjectTasks, ...result.tasks]
153
+ return {
154
+ ...result,
155
+ readyForIngest: result.readyForIngest && pendingProjectTasks.length === 0,
156
+ tasks,
157
+ }
158
+ }
159
+ }
160
+ const roots = unknownKeys.map((rootKey) => {
161
+ const judgment = latest.get(rootKey)
162
+ validateClassificationJudgment(judgment, rootKey)
163
+ return {
164
+ rootKey,
165
+ projectName: judgment.projectName,
166
+ workIntent: judgment.workIntent,
167
+ groupingBasis: judgment.groupingBasis,
168
+ ...(judgment.targetProjectId ? { targetProjectId: judgment.targetProjectId } : {}),
169
+ }
170
+ })
171
+ const candidatesByName = new Map()
172
+ for (const root of roots) {
173
+ const projectName = root.projectName.trim().normalize('NFC')
174
+ const name = projectName.toLocaleLowerCase('en-US')
175
+ const identity = JSON.stringify([root.targetProjectId ?? null, name])
176
+ let candidate = candidatesByName.get(identity)
177
+ if (!candidate) {
178
+ candidate = {
179
+ key: `c${candidatesByName.size + 1}`,
180
+ projectName,
181
+ ...(root.targetProjectId ? { targetProjectId: root.targetProjectId } : {}),
182
+ roots: [],
183
+ }
184
+ candidatesByName.set(identity, candidate)
185
+ }
186
+ candidate.roots.push([
187
+ root.rootKey,
188
+ root.workIntent,
189
+ root.groupingBasis,
190
+ ...(root.targetProjectId ? [root.targetProjectId] : []),
191
+ ])
192
+ }
193
+ const catalog = manifest.classification.projects.map((project) => ({
194
+ projectId: project.projectId,
195
+ projectName: project.projectName,
196
+ }))
197
+ const candidates = [...candidatesByName.values()]
198
+ const rootContext = manifest.classification.unknown.map((root) => [
199
+ root.rootKey,
200
+ root.metadata?.workspaceKind ?? null,
201
+ root.metadata?.workspaceName ?? null,
202
+ root.metadata?.projectEvidence ?? null,
203
+ ])
204
+ const data = { catalog, rootContext, candidates }
205
+ if (taskTokens(directory, 'consolidation', instruction, data, formatPayload) > budget)
206
+ throw new Error('CLASSIFICATION_CONSOLIDATION_TOO_LARGE')
207
+ const consolidationId = refresh ? `consolidation-${randomUUID()}` : 'consolidation'
208
+ manifest.consolidation = await task(directory, consolidationId, instruction, data, formatPayload)
209
+ const descriptor = JSON.parse(await readFile(manifest.consolidation.descriptor, 'utf8'))
210
+ descriptor.candidates = candidates
211
+ descriptor.projectCatalog = catalog
212
+ await save(manifest.consolidation.descriptor, descriptor)
213
+ manifest.consolidation.candidates = candidates
214
+ manifest.consolidation.projectCatalog = catalog
215
+ await save(join(directory, 'manifest.json'), manifest)
216
+ return {
217
+ stage: 'classification',
218
+ consolidationReady: true,
219
+ readyForIngest: false,
220
+ tasks: [dispatchDescriptor(manifest.consolidation)],
221
+ }
222
+ }
223
+
224
+ export {
225
+ classificationSubmission,
226
+ partialClassificationSubmission,
227
+ } from './journey-consolidation-submit.mjs'
@@ -0,0 +1,143 @@
1
+ import { join } from 'node:path'
2
+ import { hash, optional, save } from './journey-state.mjs'
3
+
4
+ const selected = (view) =>
5
+ view.works.filter((work) => work.access !== 'drop' && work.rootSessionIds.length)
6
+
7
+ function layout(view) {
8
+ return view.works
9
+ .map((work) => ({
10
+ id: work.id,
11
+ inputRevision: work.inputRevision,
12
+ access: work.access,
13
+ roots: [...work.rootSessionIds].sort((a, b) => a.localeCompare(b)),
14
+ }))
15
+ .sort((a, b) => a.id.localeCompare(b.id))
16
+ }
17
+
18
+ function binding(view, journeyId) {
19
+ const roots = view.works
20
+ .flatMap((work) => work.rootSessionIds.map((id) => [id, work.access]))
21
+ .sort((a, b) => a[0].localeCompare(b[0]))
22
+ if (new Set(roots.map(([id]) => id)).size !== roots.length)
23
+ throw new Error('SCAN_TIMING_DUPLICATE_ROOT')
24
+ return hash({
25
+ journeyId,
26
+ snapshotId: view.snapshotId,
27
+ classificationRevision: view.classificationRevision,
28
+ configurationId: view.configurationId,
29
+ roots,
30
+ })
31
+ }
32
+
33
+ function complete(view) {
34
+ if (
35
+ !view.consent ||
36
+ view.scope.status !== 'confirmed' ||
37
+ !view.readingComplete ||
38
+ view.phase === 'paused'
39
+ )
40
+ return false
41
+ if (
42
+ selected(view).some(
43
+ (work) => ['pending', 'blocked'].includes(work.status) || !work.classificationMetadata,
44
+ )
45
+ )
46
+ return false
47
+ const pending = view.privacy.terms
48
+ .filter((term) => term.disposition === 'pending')
49
+ .map((term) => JSON.stringify([term.type, term.value.normalize('NFC')]))
50
+ if (!pending.length) return true
51
+ const cards = view.privacy.cards
52
+ if (!cards.length || cards.length > 20) return false
53
+ const covered = cards.flatMap((card) => card.termKeys)
54
+ return (
55
+ covered.length === pending.length &&
56
+ new Set(covered).size === covered.length &&
57
+ pending.every((key) => covered.includes(key))
58
+ )
59
+ }
60
+
61
+ /** Observe the complete static + AI + metadata + cards wall interval without owning preparation. */
62
+ export async function scanTiming(run, journeyId, view, start = false, call) {
63
+ const path = join(run, 'scan-timing.json')
64
+ let receipt = await optional(path)
65
+ const identity = binding(view, journeyId)
66
+ if (!receipt && !start) return null
67
+ const guidance = !receipt?.completedAt
68
+ ? (await call('review.guidance', { journeyId })).guidance
69
+ : null
70
+ const authority = guidance
71
+ ? hash({
72
+ instructions: guidance.instructions,
73
+ valueOverrides: guidance.valueOverrides,
74
+ rules: guidance.rules,
75
+ conditions: guidance.conditions.filter((condition) =>
76
+ ['explicit', 'delegate', 'preference'].includes(condition.origin),
77
+ ),
78
+ })
79
+ : null
80
+ if (!receipt) {
81
+ if (
82
+ !view.consent ||
83
+ !view.snapshotId ||
84
+ view.classificationRevision === null ||
85
+ view.scope.status !== 'confirmed' ||
86
+ view.phase === 'paused' ||
87
+ !selected(view).length
88
+ )
89
+ throw new Error('SCAN_TIMING_NOT_ALLOWED')
90
+ if (
91
+ view.readingComplete ||
92
+ view.privacy.terms.length ||
93
+ view.privacy.cards.length ||
94
+ view.works.some(
95
+ (work) =>
96
+ work.status !== 'pending' ||
97
+ work.totalBatches ||
98
+ work.completedBatches ||
99
+ work.emptyReview ||
100
+ work.classificationMetadata ||
101
+ work.metadataReviewedDigest ||
102
+ work.artifact,
103
+ )
104
+ )
105
+ throw new Error('SCAN_TIMING_ALREADY_PREPARED')
106
+ receipt = {
107
+ binding: identity,
108
+ authority,
109
+ initialGuidanceDigest: view.guidanceDigest,
110
+ startedAt: new Date().toISOString(),
111
+ completedAt: null,
112
+ startRevision: view.revision,
113
+ snapshotId: view.snapshotId,
114
+ initialWorks: layout(view),
115
+ }
116
+ await save(path, receipt)
117
+ }
118
+ if (receipt.binding !== identity || (authority !== null && receipt.authority !== authority))
119
+ throw new Error('SCAN_TIMING_SCOPE_CHANGED')
120
+ if (!receipt.completedAt && complete(view)) {
121
+ receipt = {
122
+ ...receipt,
123
+ completedAt: new Date().toISOString(),
124
+ endRevision: view.revision,
125
+ finalWorks: layout(view),
126
+ finalGuidanceDigest: view.guidanceDigest,
127
+ }
128
+ await save(path, receipt)
129
+ }
130
+ const elapsedMs =
131
+ Date.parse(receipt.completedAt ?? new Date().toISOString()) - Date.parse(receipt.startedAt)
132
+ const { initialWorks, finalWorks, ...summary } = receipt
133
+ return {
134
+ ...summary,
135
+ initialWorkCount: initialWorks.length,
136
+ ...(finalWorks ? { finalWorkCount: finalWorks.length } : {}),
137
+ receipt: path,
138
+ status: receipt.completedAt ? 'complete' : 'running',
139
+ elapsedMs,
140
+ budgetMs: 30 * 60 * 1000,
141
+ withinBudget: receipt.completedAt ? elapsedMs < 30 * 60 * 1000 : null,
142
+ }
143
+ }
@@ -0,0 +1,43 @@
1
+ export function save(path: string, value: unknown): Promise<void>
2
+ export function cardBatchOpen(run: string): Promise<boolean>
3
+ export function hash(value: unknown): string
4
+ export function definiteWriteRejection(error: unknown): boolean
5
+ export function stageFingerprint(view: unknown, stage: string): string
6
+ export function planResolutions(
7
+ view: {
8
+ works: {
9
+ access: string
10
+ rootSessionIds: string[]
11
+ projectName?: string
12
+ classificationMetadata?: { projectName?: string }
13
+ }[]
14
+ privacy: {
15
+ terms: { type: string; value: string; disposition: string }[]
16
+ cards?: { termKeys: string[]; answer?: { optionId: string } | null }[]
17
+ }
18
+ valueOverrides?: { value: string; choice: 'hide' | 'keep' }[]
19
+ },
20
+ resolutions: {
21
+ type: string
22
+ value: string
23
+ disposition: 'hidden' | 'keep'
24
+ reason: string
25
+ }[],
26
+ ): {
27
+ id: string
28
+ resolutions: { type: string; value: string; disposition: 'hidden' | 'keep'; reason: string }[]
29
+ }[]
30
+ export function recoverWrites(
31
+ directory: string,
32
+ manifest: { stage: string; tasks?: { id: string }[]; resolutionBatches?: { id: string }[] },
33
+ call: (
34
+ procedure: string,
35
+ request: unknown,
36
+ ) => Promise<{ status: string; state?: { revision: number } }>,
37
+ ): Promise<{
38
+ recovered: number
39
+ completed: number
40
+ total: number
41
+ stale: boolean
42
+ rejected: string | null
43
+ }>