@work-graph/cli 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 (194) hide show
  1. package/README.md +31 -0
  2. package/bin/work-graph.mjs +238 -0
  3. package/package.json +38 -0
  4. package/vendor/packages/design-tokens/generated/gripe-dark-default.css +67 -0
  5. package/vendor/packages/design-tokens/generated/marketplace-default.css +67 -0
  6. package/vendor/packages/design-tokens/generated/workgraph-dark.css +67 -0
  7. package/vendor/packages/workgraph-mcp/README.md +28 -0
  8. package/vendor/packages/workgraph-mcp/bin/workgraph-mcp.mjs +21 -0
  9. package/vendor/packages/workgraph-mcp/package.json +37 -0
  10. package/vendor/packages/workgraph-mcp/src/handlers.mjs +761 -0
  11. package/vendor/packages/workgraph-mcp/src/index.mjs +638 -0
  12. package/vendor/packages/workgraph-mcp/src/prompts.mjs +162 -0
  13. package/vendor/public/assets/workgraph-logo.svg +11 -0
  14. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Medium.woff2 +0 -0
  15. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Regular.woff2 +0 -0
  16. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Semibold.woff2 +0 -0
  17. package/vendor/public/fonts/GraphikLCG/stylesheet.css +25 -0
  18. package/vendor/public/graph-canvas-lit-flow.css +154 -0
  19. package/vendor/public/graph-canvas-lit-flow.css.map +7 -0
  20. package/vendor/public/graph-canvas-lit-flow.js +8530 -0
  21. package/vendor/public/graph-canvas-lit-flow.js.map +7 -0
  22. package/vendor/src/agentBehaviorRulesAudit.mjs +168 -0
  23. package/vendor/src/agentBehaviorRulesBundle.mjs +144 -0
  24. package/vendor/src/agentRunApi.mjs +136 -0
  25. package/vendor/src/agentToolLoopGuard.mjs +88 -0
  26. package/vendor/src/agentWorkerClaudeProvider.mjs +288 -0
  27. package/vendor/src/agentWorkerCursorSdkProvider.mjs +156 -0
  28. package/vendor/src/agentWorkerLiveLoop.mjs +455 -0
  29. package/vendor/src/agentWorkerLocalCliProvider.mjs +217 -0
  30. package/vendor/src/agentWorkerLocalRunner.mjs +246 -0
  31. package/vendor/src/agentWorkerOpenAiProvider.mjs +459 -0
  32. package/vendor/src/analyticsPanelProjection.mjs +212 -0
  33. package/vendor/src/analyticsRecordStore.mjs +165 -0
  34. package/vendor/src/analyticsRecordWorkItems.mjs +104 -0
  35. package/vendor/src/architectureL1Canon.mjs +419 -0
  36. package/vendor/src/architectureLayout.mjs +229 -0
  37. package/vendor/src/architectureSnapshot.mjs +490 -0
  38. package/vendor/src/architectureViewsProjection.mjs +116 -0
  39. package/vendor/src/atomInspector.mjs +253 -0
  40. package/vendor/src/atomInspectorApi.mjs +130 -0
  41. package/vendor/src/auditGapMatrixRefresh.mjs +121 -0
  42. package/vendor/src/backlogSchemaLint.mjs +176 -0
  43. package/vendor/src/blockedOnebaseGoPreflightEval.mjs +100 -0
  44. package/vendor/src/bracketIrTraceSignal.mjs +93 -0
  45. package/vendor/src/bvcAtomParser.mjs +210 -0
  46. package/vendor/src/bvcDialectRegistry.mjs +86 -0
  47. package/vendor/src/bvcFileFormat.mjs +218 -0
  48. package/vendor/src/bvcFormatCli.mjs +55 -0
  49. package/vendor/src/bvcLintCli.mjs +48 -0
  50. package/vendor/src/bvcNewWritePolicy.mjs +70 -0
  51. package/vendor/src/charterPreflightPromoteGate.mjs +194 -0
  52. package/vendor/src/claimNoEligibleEval.mjs +205 -0
  53. package/vendor/src/closingAnalysisSuggest.mjs +59 -0
  54. package/vendor/src/codeGapAnalyzer.mjs +308 -0
  55. package/vendor/src/codeGapBacklogFeeder.mjs +82 -0
  56. package/vendor/src/codeGapDraftIntakeApi.mjs +307 -0
  57. package/vendor/src/codeGapOperatorProjection.mjs +60 -0
  58. package/vendor/src/codeSyntaxHighlight.mjs +123 -0
  59. package/vendor/src/codegenEvidence.mjs +187 -0
  60. package/vendor/src/compilerRoundTripCli.mjs +164 -0
  61. package/vendor/src/dagreGraphLayout.mjs +78 -0
  62. package/vendor/src/draftIntakePromotionRules.mjs +205 -0
  63. package/vendor/src/epicWorkScope.mjs +85 -0
  64. package/vendor/src/evalLiveLlmEnv.mjs +63 -0
  65. package/vendor/src/evidenceReadModel.mjs +167 -0
  66. package/vendor/src/gfsOverlayProjectPassport.mjs +235 -0
  67. package/vendor/src/globalStepPathToBvcReferences.mjs +196 -0
  68. package/vendor/src/goldenPath.mjs +69 -0
  69. package/vendor/src/graphCanvasLayout.mjs +464 -0
  70. package/vendor/src/graphCanvasLitFlow/client/graphCanvasMinimap.ts +261 -0
  71. package/vendor/src/graphCanvasLitFlow/client/graphCanvasSvgEdges.ts +259 -0
  72. package/vendor/src/graphCanvasLitFlow/client/graphCanvasTheme.css +152 -0
  73. package/vendor/src/graphCanvasLitFlow/client/graphCardNode.ts +328 -0
  74. package/vendor/src/graphCanvasLitFlow/client/mountGraphCanvasLitFlow.ts +322 -0
  75. package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeLabels.mjs +58 -0
  76. package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeRouter.mjs +142 -0
  77. package/vendor/src/graphCanvasLitFlow/graphCanvasLayoutProfile.mjs +32 -0
  78. package/vendor/src/graphCanvasLitFlow/graphCanvasNodeMetrics.mjs +45 -0
  79. package/vendor/src/graphCanvasLitFlow/graphCanvasProjection.mjs +115 -0
  80. package/vendor/src/graphCanvasLitFlow/graphCanvasProjectionToFlow.mjs +133 -0
  81. package/vendor/src/graphCanvasLitFlow/graphCanvasTraversal.mjs +77 -0
  82. package/vendor/src/graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs +73 -0
  83. package/vendor/src/graphCanvasLitFlow/resolveGraphCanvasOverlaps.mjs +77 -0
  84. package/vendor/src/graphRagContextSlice.mjs +461 -0
  85. package/vendor/src/gvmVerifyWorkerGate.mjs +95 -0
  86. package/vendor/src/homeSnapshotApi.mjs +131 -0
  87. package/vendor/src/homeSnapshotProjection.mjs +275 -0
  88. package/vendor/src/inboxEventStream.mjs +140 -0
  89. package/vendor/src/intentComposerApi.mjs +245 -0
  90. package/vendor/src/intentGraphGbcSliceBoundary.mjs +258 -0
  91. package/vendor/src/intentGraphProjection.mjs +208 -0
  92. package/vendor/src/intentHierarchy.mjs +241 -0
  93. package/vendor/src/intentNodeLint.mjs +107 -0
  94. package/vendor/src/intentNodeRuntime.mjs +185 -0
  95. package/vendor/src/intentRoadmapCanvas.mjs +393 -0
  96. package/vendor/src/intentRoadmapEpicProjection.mjs +122 -0
  97. package/vendor/src/intentRoadmapMermaid.mjs +165 -0
  98. package/vendor/src/intentRoadmapProjection.mjs +85 -0
  99. package/vendor/src/intentTreeLint.mjs +114 -0
  100. package/vendor/src/intentTreeMigration.mjs +150 -0
  101. package/vendor/src/intentTreeWorkItems.mjs +227 -0
  102. package/vendor/src/kanbanBoardProjection.mjs +58 -0
  103. package/vendor/src/languageAdapterRegistry.mjs +180 -0
  104. package/vendor/src/languageAdapters/goAdapter.mjs +62 -0
  105. package/vendor/src/languageAdapters/jsTsAdapter.mjs +60 -0
  106. package/vendor/src/languageAdapters/jsonYamlAdapter.mjs +103 -0
  107. package/vendor/src/languageAdapters/onebaseOsAdapter.mjs +55 -0
  108. package/vendor/src/languageAdapters/plaintextAdapter.mjs +36 -0
  109. package/vendor/src/languageAdapters/shared.mjs +68 -0
  110. package/vendor/src/languageAdapters/stepAdapter.mjs +81 -0
  111. package/vendor/src/lintPlanWorkAlignment.mjs +136 -0
  112. package/vendor/src/loopHintRepeatToolEval.mjs +153 -0
  113. package/vendor/src/lowcodeScaffoldCli.mjs +386 -0
  114. package/vendor/src/markdownDocumentRender.mjs +208 -0
  115. package/vendor/src/memoryPanelProjection.mjs +116 -0
  116. package/vendor/src/memoryRecordWriter.mjs +243 -0
  117. package/vendor/src/memoryWorkerSlice.mjs +238 -0
  118. package/vendor/src/migrateStepToBvc.mjs +133 -0
  119. package/vendor/src/missionControlServerHandlers.mjs +195 -0
  120. package/vendor/src/missionControlUiClient.mjs +278 -0
  121. package/vendor/src/onebaseCliCapabilityProbe.mjs +107 -0
  122. package/vendor/src/onebaseCliRunner.mjs +145 -0
  123. package/vendor/src/onebaseGrossProfitStaticVerify.mjs +98 -0
  124. package/vendor/src/onebaseParityEvidenceSync.mjs +88 -0
  125. package/vendor/src/onebasePvrgGraphNodes.mjs +257 -0
  126. package/vendor/src/onebaseRestEvidenceAdapter.mjs +216 -0
  127. package/vendor/src/onebaseVectorDslCodegenReadiness.mjs +137 -0
  128. package/vendor/src/onebaseWorkItemTemplate.mjs +154 -0
  129. package/vendor/src/onebaseWorkerTools.mjs +586 -0
  130. package/vendor/src/operatorShellProjection.mjs +102 -0
  131. package/vendor/src/pipelineProseRender.mjs +180 -0
  132. package/vendor/src/pipelineStageLint.mjs +118 -0
  133. package/vendor/src/promptRulesEditorApi.mjs +174 -0
  134. package/vendor/src/promptRulesProjection.mjs +134 -0
  135. package/vendor/src/pvrg/bladeAdapter.mjs +40 -0
  136. package/vendor/src/pvrgTaskScope.mjs +152 -0
  137. package/vendor/src/releaseGateMatrix.mjs +188 -0
  138. package/vendor/src/schematicView.mjs +305 -0
  139. package/vendor/src/seedAnalyticsRecord.mjs +217 -0
  140. package/vendor/src/semanticSearchBm25.mjs +103 -0
  141. package/vendor/src/semanticSearchExcerpts.mjs +68 -0
  142. package/vendor/src/semanticSearchTfidfVector.mjs +86 -0
  143. package/vendor/src/semanticSearchWorkflow.mjs +366 -0
  144. package/vendor/src/stepAtomFormatter.mjs +413 -0
  145. package/vendor/src/stepGraphSlice.mjs +318 -0
  146. package/vendor/src/ui/atoms/badge.mjs +40 -0
  147. package/vendor/src/ui/atoms/badgeClient.mjs +32 -0
  148. package/vendor/src/ui/atoms/button.mjs +114 -0
  149. package/vendor/src/ui/atoms/buttonClient.mjs +49 -0
  150. package/vendor/src/ui/atoms/icon.mjs +23 -0
  151. package/vendor/src/ui/atoms/input.mjs +38 -0
  152. package/vendor/src/ui/atoms/modal.mjs +44 -0
  153. package/vendor/src/ui/atoms/select.mjs +98 -0
  154. package/vendor/src/ui/backlogShellButtons.mjs +238 -0
  155. package/vendor/src/ui/htmlEscape.mjs +11 -0
  156. package/vendor/src/ui/molecules/rating.mjs +48 -0
  157. package/vendor/src/ui/molecules/tabs.mjs +70 -0
  158. package/vendor/src/ui/organisms/modal.mjs +1 -0
  159. package/vendor/src/ui/pages/uiKitPage.mjs +147 -0
  160. package/vendor/src/ui/workItemStatusTone.mjs +36 -0
  161. package/vendor/src/unifiedLinkageProjection.mjs +264 -0
  162. package/vendor/src/verificationLoop.mjs +206 -0
  163. package/vendor/src/workGraphBacklogPersist.mjs +234 -0
  164. package/vendor/src/workGraphBacklogUiServer.mjs +9192 -0
  165. package/vendor/src/workGraphBoundedTargetFileRead.mjs +178 -0
  166. package/vendor/src/workGraphCycleSlice.mjs +184 -0
  167. package/vendor/src/workGraphDaemonTick.mjs +307 -0
  168. package/vendor/src/workGraphDaemonWatch.mjs +157 -0
  169. package/vendor/src/workGraphEngineRoot.mjs +136 -0
  170. package/vendor/src/workGraphInstallLayout.mjs +65 -0
  171. package/vendor/src/workGraphLlmUsefulnessEval.mjs +611 -0
  172. package/vendor/src/workGraphPhasePromoteReadyQueue.mjs +159 -0
  173. package/vendor/src/workGraphProjectHost.mjs +149 -0
  174. package/vendor/src/workGraphProjectInit.mjs +392 -0
  175. package/vendor/src/workGraphPromoteReadyApi.mjs +115 -0
  176. package/vendor/src/workGraphRecoveryPolicy.mjs +124 -0
  177. package/vendor/src/workGraphRunnerQueueProjection.mjs +187 -0
  178. package/vendor/src/workGraphRuntime.mjs +1008 -0
  179. package/vendor/src/workGraphToolSurfaceAudit.mjs +372 -0
  180. package/vendor/src/workGraphToolTransportRuntime.mjs +195 -0
  181. package/vendor/src/workGraphWorkerProvider.mjs +600 -0
  182. package/vendor/src/workItemBvcQuality.mjs +262 -0
  183. package/vendor/src/workItemCreateAnalysis.mjs +157 -0
  184. package/vendor/src/workItemDecisionPipeline.mjs +278 -0
  185. package/vendor/src/workItemEpicCascade.mjs +176 -0
  186. package/vendor/src/workItemExecutionGate.mjs +78 -0
  187. package/vendor/src/workItemHierarchy.mjs +226 -0
  188. package/vendor/src/workItemProseLint.mjs +133 -0
  189. package/vendor/src/workItemTextRusify.mjs +794 -0
  190. package/vendor/src/workItemTraceEnvelope.mjs +158 -0
  191. package/vendor/src/workItemUiReferences.mjs +272 -0
  192. package/vendor/src/workflowEpicGrouping.mjs +67 -0
  193. package/vendor/src/workflowTreeProjection.mjs +53 -0
  194. package/vendor/src/workspaceRegistry.mjs +150 -0
@@ -0,0 +1,212 @@
1
+ import { readAnalyticsRecordJournal } from './analyticsRecordStore.mjs';
2
+ import { attachRelatedWorkItemsToAnalyticsRecords } from './analyticsRecordWorkItems.mjs';
3
+ import { readWorkItemsFromRepo } from './intentTreeWorkItems.mjs';
4
+ import { buildIntentGraphProjection, attachIntentGraphToAnalyticsRecords } from './intentGraphProjection.mjs';
5
+ import { readIntentNodesFromRepo } from './intentNodeRuntime.mjs';
6
+
7
+ export const ANALYTICS_PANEL_PROJECTION_SCHEMA = 'analytics-panel.projection.v1';
8
+ export const ANALYTICS_RECORDS_API_SCHEMA = 'analytics-records.api.v1';
9
+ export const DEFAULT_ANALYTICS_RECORDS_LIMIT = 50;
10
+ export const ANALYTICS_RECORD_KIND_INTAKE = 'intake';
11
+ export const ANALYTICS_RECORD_KIND_CLOSING = 'closing';
12
+
13
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
14
+
15
+ /**
16
+ * Intake AN (prospective analysis) vs closing AN (epic post-mortem).
17
+ * Do not use feeds_epics or closing-analysis tag alone — intake may discuss or feed epics (AN-22, AN-25).
18
+ */
19
+ export function inferAnalyticsRecordKind(record) {
20
+ if (!record || typeof record !== 'object') {
21
+ return ANALYTICS_RECORD_KIND_INTAKE;
22
+ }
23
+
24
+ const id = String(record.id ?? '').trim().toLowerCase();
25
+ const bodyPath = String(record.bodyPath ?? '').trim().toLowerCase();
26
+ const title = String(record.title ?? '').trim().toLowerCase();
27
+
28
+ if (
29
+ id.includes('closing-')
30
+ || bodyPath.includes('/closing-')
31
+ || bodyPath.includes('closing-epic')
32
+ ) {
33
+ return ANALYTICS_RECORD_KIND_CLOSING;
34
+ }
35
+
36
+ if (/^an-\d+:\s*closing\b/iu.test(title)) {
37
+ return ANALYTICS_RECORD_KIND_CLOSING;
38
+ }
39
+
40
+ return ANALYTICS_RECORD_KIND_INTAKE;
41
+ }
42
+
43
+ export function attachAnalyticsRecordKind(record) {
44
+ return {
45
+ ...record,
46
+ recordKind: inferAnalyticsRecordKind(record),
47
+ };
48
+ }
49
+
50
+ export function filterAnalyticsRecordsByKind(records, kind) {
51
+ if (!Array.isArray(records)) {
52
+ throw new TypeError('records must be an array');
53
+ }
54
+
55
+ const normalizedKind = String(kind ?? '').trim();
56
+ if (normalizedKind === '') {
57
+ return records;
58
+ }
59
+
60
+ return records.filter((record) => inferAnalyticsRecordKind(record) === normalizedKind);
61
+ }
62
+
63
+ function summarizeRecords(records) {
64
+ const byTopic = {};
65
+ const byStatus = {};
66
+ const byKind = {
67
+ [ANALYTICS_RECORD_KIND_INTAKE]: 0,
68
+ [ANALYTICS_RECORD_KIND_CLOSING]: 0,
69
+ };
70
+
71
+ for (const record of records) {
72
+ byTopic[record.topic] = (byTopic[record.topic] ?? 0) + 1;
73
+ byStatus[record.status] = (byStatus[record.status] ?? 0) + 1;
74
+ const kind = inferAnalyticsRecordKind(record);
75
+ byKind[kind] = (byKind[kind] ?? 0) + 1;
76
+ }
77
+
78
+ return {
79
+ total: records.length,
80
+ byTopic,
81
+ byStatus,
82
+ byKind,
83
+ };
84
+ }
85
+
86
+ export function assignAnalyticsRecordKeys(records) {
87
+ if (!Array.isArray(records)) {
88
+ throw new TypeError('records must be an array');
89
+ }
90
+
91
+ if (records.length === 0) {
92
+ return [];
93
+ }
94
+
95
+ const sorted = [...records].sort((left, right) => {
96
+ const createdCmp = String(left.createdAt ?? '').localeCompare(
97
+ String(right.createdAt ?? ''),
98
+ 'en',
99
+ { sensitivity: 'variant' },
100
+ );
101
+ if (createdCmp !== 0) {
102
+ return createdCmp;
103
+ }
104
+
105
+ return compareText(left.id, right.id);
106
+ });
107
+
108
+ const ordinalById = new Map();
109
+ sorted.forEach((record, index) => {
110
+ ordinalById.set(record.id, index + 1);
111
+ });
112
+
113
+ return records.map((record) => ({
114
+ ...record,
115
+ key: String(record.key ?? '').trim() || `AN-${ordinalById.get(record.id)}`,
116
+ }));
117
+ }
118
+
119
+ export function sortAnalyticsRecordsByRecencyDesc(records) {
120
+ if (!Array.isArray(records)) {
121
+ throw new TypeError('records must be an array');
122
+ }
123
+
124
+ return [...records].sort((left, right) => {
125
+ const createdCmp = String(right.createdAt ?? '').localeCompare(
126
+ String(left.createdAt ?? ''),
127
+ 'en',
128
+ { sensitivity: 'variant' },
129
+ );
130
+ if (createdCmp !== 0) {
131
+ return createdCmp;
132
+ }
133
+
134
+ return compareText(right.id, left.id);
135
+ });
136
+ }
137
+
138
+ export function filterAnalyticsRecords(records, options = {}) {
139
+ if (!Array.isArray(records)) {
140
+ throw new TypeError('records must be an array');
141
+ }
142
+
143
+ let filtered = records;
144
+ const topic = String(options.topic ?? '').trim();
145
+ if (topic !== '') {
146
+ filtered = filtered.filter((record) => record.topic === topic);
147
+ }
148
+
149
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : null;
150
+ const truncated = limit !== null && filtered.length > limit;
151
+ if (limit !== null) {
152
+ filtered = filtered.slice(0, limit);
153
+ }
154
+
155
+ return {
156
+ records: filtered,
157
+ truncated,
158
+ limit: limit ?? filtered.length,
159
+ topic: topic || null,
160
+ };
161
+ }
162
+
163
+ export function buildAnalyticsPanelProjectionFromRecords(records, options = {}) {
164
+ const keyedRecords = assignAnalyticsRecordKeys(records).map((record) => attachAnalyticsRecordKind(record));
165
+ const summary = summarizeRecords(keyedRecords);
166
+ const orderedRecords = sortAnalyticsRecordsByRecencyDesc(keyedRecords);
167
+
168
+ return {
169
+ schema: ANALYTICS_PANEL_PROJECTION_SCHEMA,
170
+ readOnly: true,
171
+ source: options.source ?? 'analytics-records-journal',
172
+ summary,
173
+ records: orderedRecords,
174
+ filters: {
175
+ topics: Object.keys(summary.byTopic).sort(),
176
+ statuses: Object.keys(summary.byStatus).sort(),
177
+ },
178
+ };
179
+ }
180
+
181
+ export async function buildAnalyticsRecordsApiResponse(options = {}) {
182
+ const journal = await readAnalyticsRecordJournal(options);
183
+ const keyedRecords = assignAnalyticsRecordKeys(journal.records).map((record) => attachAnalyticsRecordKind(record));
184
+ const orderedRecords = sortAnalyticsRecordsByRecencyDesc(keyedRecords);
185
+ const filtered = filterAnalyticsRecords(orderedRecords, {
186
+ topic: options.topic,
187
+ limit: options.limit ?? DEFAULT_ANALYTICS_RECORDS_LIMIT,
188
+ });
189
+
190
+ return {
191
+ schema: ANALYTICS_RECORDS_API_SCHEMA,
192
+ count: filtered.records.length,
193
+ truncated: filtered.truncated,
194
+ topic: filtered.topic,
195
+ limit: filtered.limit,
196
+ records: filtered.records,
197
+ };
198
+ }
199
+
200
+ export async function buildAnalyticsPanelProjection(options = {}) {
201
+ const journal = await readAnalyticsRecordJournal(options);
202
+ const projection = buildAnalyticsPanelProjectionFromRecords(journal.records, options);
203
+ const workItems = await readWorkItemsFromRepo(options);
204
+ const intentNodes = await readIntentNodesFromRepo(options);
205
+ const intentGraph = buildIntentGraphProjection(intentNodes, workItems, options);
206
+ const withWorkItems = attachRelatedWorkItemsToAnalyticsRecords(projection.records, workItems);
207
+ return {
208
+ ...projection,
209
+ intentGraphSchema: intentGraph.schema,
210
+ records: attachIntentGraphToAnalyticsRecords(withWorkItems, intentGraph),
211
+ };
212
+ }
@@ -0,0 +1,165 @@
1
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+
4
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
5
+
6
+ export const ANALYTICS_RECORD_SCHEMA = 'analytics-record.v1';
7
+ export const ANALYTICS_RECORD_JOURNAL_SCHEMA = 'analytics-record.journal.v1';
8
+ export const ANALYTICS_RECORD_JOURNAL_READ_SCHEMA = 'analytics-record.journal.read.v1';
9
+ export const ANALYTICS_RECORD_JOURNAL_APPEND_SCHEMA = 'analytics-record.journal.append.v1';
10
+ export const DEFAULT_ANALYTICS_RECORD_JOURNAL_PATH = 'work/analytics-records.jsonl';
11
+
12
+ function stableAnalyticsId(slug) {
13
+ const normalized = String(slug ?? '')
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9]+/gu, '-')
16
+ .replace(/^-+|-+$/gu, '')
17
+ .slice(0, 48);
18
+ return `analytics:${normalized || 'record'}`;
19
+ }
20
+
21
+ export function buildAnalyticsRecord(input, options = {}) {
22
+ if (!input || typeof input !== 'object') {
23
+ throw new TypeError('input is required');
24
+ }
25
+
26
+ const title = String(input.title ?? '').trim();
27
+ const query = String(input.query ?? '').trim();
28
+ if (!title) {
29
+ throw new TypeError('title is required');
30
+ }
31
+ if (!query) {
32
+ throw new TypeError('query is required');
33
+ }
34
+
35
+ const slug = input.slug ?? title;
36
+ const createdAt = input.createdAt ?? options.appendedAt ?? new Date().toISOString();
37
+
38
+ return {
39
+ schema: ANALYTICS_RECORD_SCHEMA,
40
+ id: input.id ?? stableAnalyticsId(slug),
41
+ title,
42
+ query,
43
+ topic: String(input.topic ?? 'general').trim(),
44
+ status: input.status ?? 'published',
45
+ tags: [...(input.tags ?? [])].map(String).sort(compareText),
46
+ relatedFiles: [...(input.relatedFiles ?? [])].map(String).sort(compareText),
47
+ body: input.body ?? '',
48
+ bodyPath: input.bodyPath ?? null,
49
+ createdAt,
50
+ updatedAt: input.updatedAt ?? createdAt,
51
+ author: input.author ?? 'operator',
52
+ ...(String(input.key ?? '').trim() !== '' ? { key: String(input.key).trim() } : {}),
53
+ };
54
+ }
55
+
56
+ async function resolveRecordBody(record, options = {}) {
57
+ const bodyPath = String(record.bodyPath ?? '').trim();
58
+ if (!bodyPath) {
59
+ return String(record.body ?? '');
60
+ }
61
+
62
+ const absolutePath = resolve(options.cwd ?? process.cwd(), bodyPath);
63
+ try {
64
+ return await readFile(absolutePath, 'utf8');
65
+ } catch (error) {
66
+ if (error && typeof error === 'object' && error.code === 'ENOENT') {
67
+ return String(record.body ?? '');
68
+ }
69
+
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ export async function hydrateAnalyticsRecords(records, options = {}) {
75
+ return Promise.all(records.map(async (record) => ({
76
+ ...record,
77
+ body: await resolveRecordBody(record, options),
78
+ })));
79
+ }
80
+
81
+ export function buildAnalyticsJournalEntry(record, options = {}) {
82
+ const appendedAt = options.appendedAt ?? new Date().toISOString();
83
+ const normalized = buildAnalyticsRecord(record, { appendedAt });
84
+
85
+ return {
86
+ schema: ANALYTICS_RECORD_JOURNAL_SCHEMA,
87
+ appendedAt,
88
+ record: normalized,
89
+ };
90
+ }
91
+
92
+ export async function readAnalyticsRecordJournal(options = {}) {
93
+ const journalPath = resolve(options.cwd ?? process.cwd(), options.journalPath ?? DEFAULT_ANALYTICS_RECORD_JOURNAL_PATH);
94
+
95
+ let text = '';
96
+ try {
97
+ text = await readFile(journalPath, 'utf8');
98
+ } catch (error) {
99
+ if (error && typeof error === 'object' && error.code === 'ENOENT') {
100
+ return {
101
+ schema: ANALYTICS_RECORD_JOURNAL_READ_SCHEMA,
102
+ journalPath,
103
+ entryCount: 0,
104
+ entries: [],
105
+ records: [],
106
+ };
107
+ }
108
+
109
+ throw error;
110
+ }
111
+
112
+ const entries = text
113
+ .split(/\r?\n/u)
114
+ .map((line) => line.trim())
115
+ .filter(Boolean)
116
+ .map((line, index) => {
117
+ const parsed = JSON.parse(line);
118
+ if (parsed.schema !== ANALYTICS_RECORD_JOURNAL_SCHEMA) {
119
+ throw new Error(`invalid analytics journal entry at line ${index + 1}: expected ${ANALYTICS_RECORD_JOURNAL_SCHEMA}`);
120
+ }
121
+
122
+ return parsed;
123
+ });
124
+
125
+ const latestById = new Map();
126
+ for (const entry of entries) {
127
+ latestById.set(entry.record.id, entry.record);
128
+ }
129
+
130
+ const records = [...latestById.values()].sort((left, right) => compareText(left.id, right.id));
131
+ const hydrated = await hydrateAnalyticsRecords(records, options);
132
+
133
+ return {
134
+ schema: ANALYTICS_RECORD_JOURNAL_READ_SCHEMA,
135
+ journalPath,
136
+ entryCount: entries.length,
137
+ entries,
138
+ records: hydrated,
139
+ };
140
+ }
141
+
142
+ export async function appendAnalyticsRecordJournal(records, journalPath, options = {}) {
143
+ if (!Array.isArray(records) || records.length === 0) {
144
+ throw new TypeError('records must be a non-empty array');
145
+ }
146
+
147
+ const resolvedPath = resolve(options.cwd ?? process.cwd(), journalPath ?? DEFAULT_ANALYTICS_RECORD_JOURNAL_PATH);
148
+ const appendedAt = options.appendedAt ?? new Date().toISOString();
149
+ const entries = records.map((record) => buildAnalyticsJournalEntry(record, { appendedAt }));
150
+
151
+ if (options.dryRun !== true) {
152
+ await mkdir(dirname(resolvedPath), { recursive: true });
153
+ for (const entry of entries) {
154
+ await appendFile(resolvedPath, `${JSON.stringify(entry)}\n`, 'utf8');
155
+ }
156
+ }
157
+
158
+ return {
159
+ schema: ANALYTICS_RECORD_JOURNAL_APPEND_SCHEMA,
160
+ journalPath: resolvedPath,
161
+ appended: entries.length,
162
+ entries,
163
+ records: await hydrateAnalyticsRecords(entries.map((entry) => entry.record), options),
164
+ };
165
+ }
@@ -0,0 +1,104 @@
1
+ function normalizeLines(value) {
2
+ if (Array.isArray(value)) {
3
+ return value.map((line) => String(line).trim()).filter(Boolean);
4
+ }
5
+
6
+ return String(value ?? '')
7
+ .split(/\r?\n/u)
8
+ .map((line) => line.trim())
9
+ .filter(Boolean);
10
+ }
11
+
12
+ function workItemTextHaystack(item) {
13
+ return [
14
+ ...normalizeLines(item.basis),
15
+ ...normalizeLines(item.vector),
16
+ ...normalizeLines(item.goal),
17
+ ...normalizeLines(item.analysis),
18
+ ...normalizeLines(item.decision),
19
+ ...normalizeLines(item.checks),
20
+ ...(item.targetFiles ?? []),
21
+ item.title,
22
+ item.id,
23
+ ].join('\n');
24
+ }
25
+
26
+ export function summarizeWorkItemForAnalyticsRecord(item) {
27
+ return {
28
+ id: item.id,
29
+ title: item.title ?? item.id,
30
+ status: item.status ?? '',
31
+ department: item.department ?? '',
32
+ ownerRole: item.ownerRole ?? '',
33
+ };
34
+ }
35
+
36
+ export function workItemMatchesAnalyticsRecord(record, item) {
37
+ if (!record || !item) {
38
+ return false;
39
+ }
40
+
41
+ const recordId = String(record.id ?? '').trim();
42
+ const recordKey = String(record.key ?? '').trim();
43
+ const bodyPath = String(record.bodyPath ?? '').trim();
44
+ const slug = recordId.startsWith('analytics:') ? recordId.slice('analytics:'.length) : '';
45
+ const labels = item.labels ?? {};
46
+
47
+ if (recordId !== '' && String(labels['intake.source_ref'] ?? '').trim() === recordId) {
48
+ return true;
49
+ }
50
+
51
+ if (recordKey !== '' && String(labels['intake.analytics_key'] ?? '').trim() === recordKey) {
52
+ return true;
53
+ }
54
+
55
+ const haystack = workItemTextHaystack(item);
56
+ if (recordId !== '' && haystack.includes(recordId)) {
57
+ return true;
58
+ }
59
+
60
+ if (recordKey !== '' && (haystack.includes(`(${recordKey})`) || haystack.includes(`${recordKey},`))) {
61
+ return true;
62
+ }
63
+
64
+ if (bodyPath !== '' && haystack.includes(bodyPath)) {
65
+ return true;
66
+ }
67
+
68
+ if (slug !== '' && haystack.includes(`analytics:${slug}`)) {
69
+ return true;
70
+ }
71
+
72
+ const questionId = String(labels['intent.question_id'] ?? '').trim();
73
+ const decisionId = String(labels['intent.decision_id'] ?? '').trim();
74
+ if (recordKey === 'AN-3' && questionId === 'iq:intent-graph-storage') {
75
+ return true;
76
+ }
77
+ if (recordId !== '' && decisionId !== '' && recordId.includes('intent-graph-storage')) {
78
+ return true;
79
+ }
80
+
81
+ return false;
82
+ }
83
+
84
+ export function findWorkItemsForAnalyticsRecord(record, workItems) {
85
+ if (!Array.isArray(workItems)) {
86
+ return [];
87
+ }
88
+
89
+ return workItems
90
+ .filter((item) => workItemMatchesAnalyticsRecord(record, item))
91
+ .map((item) => summarizeWorkItemForAnalyticsRecord(item))
92
+ .sort((left, right) => left.id.localeCompare(right.id, 'en', { sensitivity: 'variant' }));
93
+ }
94
+
95
+ export function attachRelatedWorkItemsToAnalyticsRecords(records, workItems) {
96
+ if (!Array.isArray(records)) {
97
+ throw new TypeError('records must be an array');
98
+ }
99
+
100
+ return records.map((record) => ({
101
+ ...record,
102
+ relatedWorkItems: findWorkItemsForAnalyticsRecord(record, workItems),
103
+ }));
104
+ }