@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.
- package/README.md +31 -0
- package/bin/work-graph.mjs +238 -0
- package/package.json +38 -0
- package/vendor/packages/design-tokens/generated/gripe-dark-default.css +67 -0
- package/vendor/packages/design-tokens/generated/marketplace-default.css +67 -0
- package/vendor/packages/design-tokens/generated/workgraph-dark.css +67 -0
- package/vendor/packages/workgraph-mcp/README.md +28 -0
- package/vendor/packages/workgraph-mcp/bin/workgraph-mcp.mjs +21 -0
- package/vendor/packages/workgraph-mcp/package.json +37 -0
- package/vendor/packages/workgraph-mcp/src/handlers.mjs +761 -0
- package/vendor/packages/workgraph-mcp/src/index.mjs +638 -0
- package/vendor/packages/workgraph-mcp/src/prompts.mjs +162 -0
- package/vendor/public/assets/workgraph-logo.svg +11 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Medium.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Regular.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Semibold.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/stylesheet.css +25 -0
- package/vendor/public/graph-canvas-lit-flow.css +154 -0
- package/vendor/public/graph-canvas-lit-flow.css.map +7 -0
- package/vendor/public/graph-canvas-lit-flow.js +8530 -0
- package/vendor/public/graph-canvas-lit-flow.js.map +7 -0
- package/vendor/src/agentBehaviorRulesAudit.mjs +168 -0
- package/vendor/src/agentBehaviorRulesBundle.mjs +144 -0
- package/vendor/src/agentRunApi.mjs +136 -0
- package/vendor/src/agentToolLoopGuard.mjs +88 -0
- package/vendor/src/agentWorkerClaudeProvider.mjs +288 -0
- package/vendor/src/agentWorkerCursorSdkProvider.mjs +156 -0
- package/vendor/src/agentWorkerLiveLoop.mjs +455 -0
- package/vendor/src/agentWorkerLocalCliProvider.mjs +217 -0
- package/vendor/src/agentWorkerLocalRunner.mjs +246 -0
- package/vendor/src/agentWorkerOpenAiProvider.mjs +459 -0
- package/vendor/src/analyticsPanelProjection.mjs +212 -0
- package/vendor/src/analyticsRecordStore.mjs +165 -0
- package/vendor/src/analyticsRecordWorkItems.mjs +104 -0
- package/vendor/src/architectureL1Canon.mjs +419 -0
- package/vendor/src/architectureLayout.mjs +229 -0
- package/vendor/src/architectureSnapshot.mjs +490 -0
- package/vendor/src/architectureViewsProjection.mjs +116 -0
- package/vendor/src/atomInspector.mjs +253 -0
- package/vendor/src/atomInspectorApi.mjs +130 -0
- package/vendor/src/auditGapMatrixRefresh.mjs +121 -0
- package/vendor/src/backlogSchemaLint.mjs +176 -0
- package/vendor/src/blockedOnebaseGoPreflightEval.mjs +100 -0
- package/vendor/src/bracketIrTraceSignal.mjs +93 -0
- package/vendor/src/bvcAtomParser.mjs +210 -0
- package/vendor/src/bvcDialectRegistry.mjs +86 -0
- package/vendor/src/bvcFileFormat.mjs +218 -0
- package/vendor/src/bvcFormatCli.mjs +55 -0
- package/vendor/src/bvcLintCli.mjs +48 -0
- package/vendor/src/bvcNewWritePolicy.mjs +70 -0
- package/vendor/src/charterPreflightPromoteGate.mjs +194 -0
- package/vendor/src/claimNoEligibleEval.mjs +205 -0
- package/vendor/src/closingAnalysisSuggest.mjs +59 -0
- package/vendor/src/codeGapAnalyzer.mjs +308 -0
- package/vendor/src/codeGapBacklogFeeder.mjs +82 -0
- package/vendor/src/codeGapDraftIntakeApi.mjs +307 -0
- package/vendor/src/codeGapOperatorProjection.mjs +60 -0
- package/vendor/src/codeSyntaxHighlight.mjs +123 -0
- package/vendor/src/codegenEvidence.mjs +187 -0
- package/vendor/src/compilerRoundTripCli.mjs +164 -0
- package/vendor/src/dagreGraphLayout.mjs +78 -0
- package/vendor/src/draftIntakePromotionRules.mjs +205 -0
- package/vendor/src/epicWorkScope.mjs +85 -0
- package/vendor/src/evalLiveLlmEnv.mjs +63 -0
- package/vendor/src/evidenceReadModel.mjs +167 -0
- package/vendor/src/gfsOverlayProjectPassport.mjs +235 -0
- package/vendor/src/globalStepPathToBvcReferences.mjs +196 -0
- package/vendor/src/goldenPath.mjs +69 -0
- package/vendor/src/graphCanvasLayout.mjs +464 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasMinimap.ts +261 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasSvgEdges.ts +259 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasTheme.css +152 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCardNode.ts +328 -0
- package/vendor/src/graphCanvasLitFlow/client/mountGraphCanvasLitFlow.ts +322 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeLabels.mjs +58 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeRouter.mjs +142 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasLayoutProfile.mjs +32 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasNodeMetrics.mjs +45 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjection.mjs +115 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjectionToFlow.mjs +133 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasTraversal.mjs +77 -0
- package/vendor/src/graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs +73 -0
- package/vendor/src/graphCanvasLitFlow/resolveGraphCanvasOverlaps.mjs +77 -0
- package/vendor/src/graphRagContextSlice.mjs +461 -0
- package/vendor/src/gvmVerifyWorkerGate.mjs +95 -0
- package/vendor/src/homeSnapshotApi.mjs +131 -0
- package/vendor/src/homeSnapshotProjection.mjs +275 -0
- package/vendor/src/inboxEventStream.mjs +140 -0
- package/vendor/src/intentComposerApi.mjs +245 -0
- package/vendor/src/intentGraphGbcSliceBoundary.mjs +258 -0
- package/vendor/src/intentGraphProjection.mjs +208 -0
- package/vendor/src/intentHierarchy.mjs +241 -0
- package/vendor/src/intentNodeLint.mjs +107 -0
- package/vendor/src/intentNodeRuntime.mjs +185 -0
- package/vendor/src/intentRoadmapCanvas.mjs +393 -0
- package/vendor/src/intentRoadmapEpicProjection.mjs +122 -0
- package/vendor/src/intentRoadmapMermaid.mjs +165 -0
- package/vendor/src/intentRoadmapProjection.mjs +85 -0
- package/vendor/src/intentTreeLint.mjs +114 -0
- package/vendor/src/intentTreeMigration.mjs +150 -0
- package/vendor/src/intentTreeWorkItems.mjs +227 -0
- package/vendor/src/kanbanBoardProjection.mjs +58 -0
- package/vendor/src/languageAdapterRegistry.mjs +180 -0
- package/vendor/src/languageAdapters/goAdapter.mjs +62 -0
- package/vendor/src/languageAdapters/jsTsAdapter.mjs +60 -0
- package/vendor/src/languageAdapters/jsonYamlAdapter.mjs +103 -0
- package/vendor/src/languageAdapters/onebaseOsAdapter.mjs +55 -0
- package/vendor/src/languageAdapters/plaintextAdapter.mjs +36 -0
- package/vendor/src/languageAdapters/shared.mjs +68 -0
- package/vendor/src/languageAdapters/stepAdapter.mjs +81 -0
- package/vendor/src/lintPlanWorkAlignment.mjs +136 -0
- package/vendor/src/loopHintRepeatToolEval.mjs +153 -0
- package/vendor/src/lowcodeScaffoldCli.mjs +386 -0
- package/vendor/src/markdownDocumentRender.mjs +208 -0
- package/vendor/src/memoryPanelProjection.mjs +116 -0
- package/vendor/src/memoryRecordWriter.mjs +243 -0
- package/vendor/src/memoryWorkerSlice.mjs +238 -0
- package/vendor/src/migrateStepToBvc.mjs +133 -0
- package/vendor/src/missionControlServerHandlers.mjs +195 -0
- package/vendor/src/missionControlUiClient.mjs +278 -0
- package/vendor/src/onebaseCliCapabilityProbe.mjs +107 -0
- package/vendor/src/onebaseCliRunner.mjs +145 -0
- package/vendor/src/onebaseGrossProfitStaticVerify.mjs +98 -0
- package/vendor/src/onebaseParityEvidenceSync.mjs +88 -0
- package/vendor/src/onebasePvrgGraphNodes.mjs +257 -0
- package/vendor/src/onebaseRestEvidenceAdapter.mjs +216 -0
- package/vendor/src/onebaseVectorDslCodegenReadiness.mjs +137 -0
- package/vendor/src/onebaseWorkItemTemplate.mjs +154 -0
- package/vendor/src/onebaseWorkerTools.mjs +586 -0
- package/vendor/src/operatorShellProjection.mjs +102 -0
- package/vendor/src/pipelineProseRender.mjs +180 -0
- package/vendor/src/pipelineStageLint.mjs +118 -0
- package/vendor/src/promptRulesEditorApi.mjs +174 -0
- package/vendor/src/promptRulesProjection.mjs +134 -0
- package/vendor/src/pvrg/bladeAdapter.mjs +40 -0
- package/vendor/src/pvrgTaskScope.mjs +152 -0
- package/vendor/src/releaseGateMatrix.mjs +188 -0
- package/vendor/src/schematicView.mjs +305 -0
- package/vendor/src/seedAnalyticsRecord.mjs +217 -0
- package/vendor/src/semanticSearchBm25.mjs +103 -0
- package/vendor/src/semanticSearchExcerpts.mjs +68 -0
- package/vendor/src/semanticSearchTfidfVector.mjs +86 -0
- package/vendor/src/semanticSearchWorkflow.mjs +366 -0
- package/vendor/src/stepAtomFormatter.mjs +413 -0
- package/vendor/src/stepGraphSlice.mjs +318 -0
- package/vendor/src/ui/atoms/badge.mjs +40 -0
- package/vendor/src/ui/atoms/badgeClient.mjs +32 -0
- package/vendor/src/ui/atoms/button.mjs +114 -0
- package/vendor/src/ui/atoms/buttonClient.mjs +49 -0
- package/vendor/src/ui/atoms/icon.mjs +23 -0
- package/vendor/src/ui/atoms/input.mjs +38 -0
- package/vendor/src/ui/atoms/modal.mjs +44 -0
- package/vendor/src/ui/atoms/select.mjs +98 -0
- package/vendor/src/ui/backlogShellButtons.mjs +238 -0
- package/vendor/src/ui/htmlEscape.mjs +11 -0
- package/vendor/src/ui/molecules/rating.mjs +48 -0
- package/vendor/src/ui/molecules/tabs.mjs +70 -0
- package/vendor/src/ui/organisms/modal.mjs +1 -0
- package/vendor/src/ui/pages/uiKitPage.mjs +147 -0
- package/vendor/src/ui/workItemStatusTone.mjs +36 -0
- package/vendor/src/unifiedLinkageProjection.mjs +264 -0
- package/vendor/src/verificationLoop.mjs +206 -0
- package/vendor/src/workGraphBacklogPersist.mjs +234 -0
- package/vendor/src/workGraphBacklogUiServer.mjs +9192 -0
- package/vendor/src/workGraphBoundedTargetFileRead.mjs +178 -0
- package/vendor/src/workGraphCycleSlice.mjs +184 -0
- package/vendor/src/workGraphDaemonTick.mjs +307 -0
- package/vendor/src/workGraphDaemonWatch.mjs +157 -0
- package/vendor/src/workGraphEngineRoot.mjs +136 -0
- package/vendor/src/workGraphInstallLayout.mjs +65 -0
- package/vendor/src/workGraphLlmUsefulnessEval.mjs +611 -0
- package/vendor/src/workGraphPhasePromoteReadyQueue.mjs +159 -0
- package/vendor/src/workGraphProjectHost.mjs +149 -0
- package/vendor/src/workGraphProjectInit.mjs +392 -0
- package/vendor/src/workGraphPromoteReadyApi.mjs +115 -0
- package/vendor/src/workGraphRecoveryPolicy.mjs +124 -0
- package/vendor/src/workGraphRunnerQueueProjection.mjs +187 -0
- package/vendor/src/workGraphRuntime.mjs +1008 -0
- package/vendor/src/workGraphToolSurfaceAudit.mjs +372 -0
- package/vendor/src/workGraphToolTransportRuntime.mjs +195 -0
- package/vendor/src/workGraphWorkerProvider.mjs +600 -0
- package/vendor/src/workItemBvcQuality.mjs +262 -0
- package/vendor/src/workItemCreateAnalysis.mjs +157 -0
- package/vendor/src/workItemDecisionPipeline.mjs +278 -0
- package/vendor/src/workItemEpicCascade.mjs +176 -0
- package/vendor/src/workItemExecutionGate.mjs +78 -0
- package/vendor/src/workItemHierarchy.mjs +226 -0
- package/vendor/src/workItemProseLint.mjs +133 -0
- package/vendor/src/workItemTextRusify.mjs +794 -0
- package/vendor/src/workItemTraceEnvelope.mjs +158 -0
- package/vendor/src/workItemUiReferences.mjs +272 -0
- package/vendor/src/workflowEpicGrouping.mjs +67 -0
- package/vendor/src/workflowTreeProjection.mjs +53 -0
- package/vendor/src/workspaceRegistry.mjs +150 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachDerivedWorkItemHierarchy,
|
|
3
|
+
DONE_STATUSES,
|
|
4
|
+
readWorkItemKind,
|
|
5
|
+
summarizeWorkItemHierarchyRollup,
|
|
6
|
+
} from './workItemHierarchy.mjs';
|
|
7
|
+
import { buildEpicRoadmapCanvasModel } from './intentRoadmapCanvas.mjs';
|
|
8
|
+
|
|
9
|
+
export const EPIC_ROADMAP_PROJECTION_SCHEMA = 'workgraph.roadmap-epics.projection.v1';
|
|
10
|
+
|
|
11
|
+
const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
|
|
12
|
+
|
|
13
|
+
function computeChildRollup(children) {
|
|
14
|
+
let closed = 0;
|
|
15
|
+
let blocked = 0;
|
|
16
|
+
let inProgress = 0;
|
|
17
|
+
|
|
18
|
+
for (const child of children) {
|
|
19
|
+
const status = String(child.status ?? '').trim().toLowerCase();
|
|
20
|
+
if (DONE_STATUSES.has(status)) {
|
|
21
|
+
closed += 1;
|
|
22
|
+
} else if (status === 'blocked') {
|
|
23
|
+
blocked += 1;
|
|
24
|
+
} else {
|
|
25
|
+
inProgress += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
closed,
|
|
31
|
+
total: children.length,
|
|
32
|
+
blocked,
|
|
33
|
+
inProgress,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildEpicChildNode(item) {
|
|
38
|
+
return {
|
|
39
|
+
workId: item.id,
|
|
40
|
+
title: item.title ?? item.id,
|
|
41
|
+
status: item.status ?? '',
|
|
42
|
+
itemKind: readWorkItemKind(item),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildEpicEntry(epic, itemById) {
|
|
47
|
+
const childItems = (epic.childIds ?? [])
|
|
48
|
+
.map((childId) => itemById.get(childId))
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
51
|
+
|
|
52
|
+
const hierarchyRollup = summarizeWorkItemHierarchyRollup(epic, [...itemById.values()]);
|
|
53
|
+
const rollup = computeChildRollup(childItems);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
epicId: epic.id,
|
|
57
|
+
title: epic.title ?? epic.id,
|
|
58
|
+
status: epic.status ?? '',
|
|
59
|
+
childCount: hierarchyRollup.childCount,
|
|
60
|
+
doneChildCount: hierarchyRollup.doneChildCount,
|
|
61
|
+
closeBlocked: hierarchyRollup.closeBlocked,
|
|
62
|
+
rollup,
|
|
63
|
+
children: childItems.map(buildEpicChildNode),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseCollapsedEpicIds(value) {
|
|
68
|
+
if (value instanceof Set) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
return new Set(value.map((entry) => String(entry).trim()).filter(Boolean));
|
|
73
|
+
}
|
|
74
|
+
const raw = String(value ?? '').trim();
|
|
75
|
+
if (raw === '') {
|
|
76
|
+
return new Set();
|
|
77
|
+
}
|
|
78
|
+
return new Set(raw.split(',').map((entry) => entry.trim()).filter(Boolean));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isActiveEpic(epic, hierarchyRollup) {
|
|
82
|
+
const status = String(epic.status ?? '').trim().toLowerCase();
|
|
83
|
+
return !DONE_STATUSES.has(status) || hierarchyRollup.closeBlocked;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {Array<object>} workItems
|
|
88
|
+
* @param {{ active?: boolean, withChildren?: boolean, collapsed?: Set<string>|string[]|string, cwd?: string }} [options]
|
|
89
|
+
*/
|
|
90
|
+
export function buildEpicRoadmapProjection(workItems, options = {}) {
|
|
91
|
+
const enriched = attachDerivedWorkItemHierarchy(workItems);
|
|
92
|
+
const itemById = new Map(enriched.map((item) => [item.id, item]));
|
|
93
|
+
const collapsedEpicIds = parseCollapsedEpicIds(options.collapsed);
|
|
94
|
+
|
|
95
|
+
let epics = enriched.filter((item) => readWorkItemKind(item) === 'epic');
|
|
96
|
+
|
|
97
|
+
if (options.active === true) {
|
|
98
|
+
epics = epics.filter((epic) => isActiveEpic(epic, summarizeWorkItemHierarchyRollup(epic, enriched)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options.withChildren === true) {
|
|
102
|
+
epics = epics.filter((epic) => (epic.childIds ?? []).length > 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entries = epics
|
|
106
|
+
.map((epic) => buildEpicEntry(epic, itemById))
|
|
107
|
+
.sort((left, right) => compareText(left.epicId, right.epicId));
|
|
108
|
+
|
|
109
|
+
const canvasOptions = { collapsedEpicIds };
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
schema: EPIC_ROADMAP_PROJECTION_SCHEMA,
|
|
113
|
+
readOnly: true,
|
|
114
|
+
epicCount: entries.length,
|
|
115
|
+
epics: entries.map((entry) => ({
|
|
116
|
+
...entry,
|
|
117
|
+
canvas: buildEpicRoadmapCanvasModel(entry, canvasOptions),
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { parseCollapsedEpicIds };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
|
|
2
|
+
|
|
3
|
+
export const INTENT_ROADMAP_MERMAID_SCHEMA = 'workgraph.intent-roadmap.mermaid.v1';
|
|
4
|
+
|
|
5
|
+
function sanitizeMermaidId(raw) {
|
|
6
|
+
return String(raw).replace(/[^a-zA-Z0-9_]/gu, '_').replace(/_+/gu, '_').replace(/^_|_$/gu, '') || 'node';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function mermaidNodeId(kind, rawId) {
|
|
10
|
+
return `${kind}_${sanitizeMermaidId(rawId)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function escapeMermaidLabel(text, maxLen = 72) {
|
|
14
|
+
const cleaned = String(text ?? '')
|
|
15
|
+
.replace(/"/gu, "'")
|
|
16
|
+
.replace(/[\[\]{}#;|]/gu, ' ')
|
|
17
|
+
.replace(/\s+/gu, ' ')
|
|
18
|
+
.trim();
|
|
19
|
+
if (cleaned.length <= maxLen) {
|
|
20
|
+
return cleaned;
|
|
21
|
+
}
|
|
22
|
+
return `${cleaned.slice(0, maxLen - 1)}…`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function analyticsTitleFromRef(analyticsRef) {
|
|
26
|
+
const ref = String(analyticsRef ?? '').trim();
|
|
27
|
+
if (ref === '') {
|
|
28
|
+
return 'Анализ';
|
|
29
|
+
}
|
|
30
|
+
const slug = ref.replace(/^analytics:/u, '');
|
|
31
|
+
return `Анализ ${slug}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findAllOptionsForQuestion(question, intentNodes) {
|
|
35
|
+
if (!question) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return intentNodes
|
|
39
|
+
.filter((node) => node.nodeKind === 'option' && node.parentId === question.id)
|
|
40
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function workNodeLabel(node) {
|
|
44
|
+
const done = node.status === 'done' || node.status === 'verified';
|
|
45
|
+
const progress = node.childCount > 0 ? ` (${node.doneChildCount}/${node.childCount})` : '';
|
|
46
|
+
const suffix = done ? ' · done' : '';
|
|
47
|
+
return escapeMermaidLabel(`${node.title ?? node.workId}${progress}${suffix}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function appendWorkTreeLines(lines, node, epicId, classNames) {
|
|
51
|
+
const taskId = mermaidNodeId('task', node.workId);
|
|
52
|
+
lines.push(` ${epicId} --> ${taskId}["${workNodeLabel(node)}"]`);
|
|
53
|
+
classNames.push(`${taskId}${doneClass(node.status)}`);
|
|
54
|
+
|
|
55
|
+
for (const child of node.children ?? []) {
|
|
56
|
+
appendWorkTreeLines(lines, child, taskId, classNames);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function doneClass(status) {
|
|
61
|
+
return status === 'done' || status === 'verified' ? ':::done' : '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {{
|
|
66
|
+
* decisionId: string,
|
|
67
|
+
* decisionTitle?: string,
|
|
68
|
+
* analyticsRef?: string,
|
|
69
|
+
* question?: { id: string, title?: string } | null,
|
|
70
|
+
* selectedOption?: { id: string, title?: string } | null,
|
|
71
|
+
* decision?: { id: string, title?: string } | null,
|
|
72
|
+
* allOptions?: Array<{ id: string, title?: string, selected?: boolean }>,
|
|
73
|
+
* roots?: Array<object>,
|
|
74
|
+
* }} branch
|
|
75
|
+
*/
|
|
76
|
+
export function buildIntentRoadmapMermaidSource(branch) {
|
|
77
|
+
const lines = ['flowchart TB'];
|
|
78
|
+
const classLines = [];
|
|
79
|
+
const classNames = [];
|
|
80
|
+
|
|
81
|
+
const question = branch.question;
|
|
82
|
+
const decision = branch.decision ?? { id: branch.decisionId, title: branch.decisionTitle };
|
|
83
|
+
const selectedOptionId = branch.selectedOption?.id ?? '';
|
|
84
|
+
const allOptions = branch.allOptions ?? (branch.selectedOption ? [branch.selectedOption] : []);
|
|
85
|
+
|
|
86
|
+
let anchorId = null;
|
|
87
|
+
|
|
88
|
+
if (question) {
|
|
89
|
+
const questionId = mermaidNodeId('q', question.id);
|
|
90
|
+
lines.push(` ${questionId}["${escapeMermaidLabel(question.title ?? question.id)}"]`);
|
|
91
|
+
classNames.push(`${questionId}:::question`);
|
|
92
|
+
anchorId = questionId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (branch.analyticsRef) {
|
|
96
|
+
const analysisId = mermaidNodeId('an', branch.analyticsRef);
|
|
97
|
+
lines.push(` ${analysisId}["${escapeMermaidLabel(analyticsTitleFromRef(branch.analyticsRef))}"]`);
|
|
98
|
+
classNames.push(`${analysisId}:::analysis`);
|
|
99
|
+
if (anchorId) {
|
|
100
|
+
lines.push(` ${anchorId} --> ${analysisId}`);
|
|
101
|
+
}
|
|
102
|
+
anchorId = analysisId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const optionIds = new Map();
|
|
106
|
+
for (const option of allOptions) {
|
|
107
|
+
const optionId = mermaidNodeId('opt', option.id);
|
|
108
|
+
optionIds.set(option.id, optionId);
|
|
109
|
+
const selected = option.id === selectedOptionId || option.selected === true;
|
|
110
|
+
lines.push(` ${optionId}["${escapeMermaidLabel(option.title ?? option.id)}"]`);
|
|
111
|
+
classNames.push(`${optionId}${selected ? ':::selected' : ':::rejected'}`);
|
|
112
|
+
if (anchorId) {
|
|
113
|
+
lines.push(` ${anchorId} --> ${optionId}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const decisionId = mermaidNodeId('dec', decision.id ?? branch.decisionId);
|
|
118
|
+
lines.push(` ${decisionId}["${escapeMermaidLabel(decision.title ?? branch.decisionTitle ?? branch.decisionId)}"]`);
|
|
119
|
+
classNames.push(`${decisionId}:::decision`);
|
|
120
|
+
|
|
121
|
+
if (selectedOptionId && optionIds.has(selectedOptionId)) {
|
|
122
|
+
lines.push(` ${optionIds.get(selectedOptionId)} --> ${decisionId}`);
|
|
123
|
+
} else if (anchorId) {
|
|
124
|
+
lines.push(` ${anchorId} --> ${decisionId}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const root of branch.roots ?? []) {
|
|
128
|
+
const epicId = mermaidNodeId('epic', root.workId);
|
|
129
|
+
lines.push(` ${decisionId} --> ${epicId}["${workNodeLabel(root)}"]`);
|
|
130
|
+
classNames.push(`${epicId}${doneClass(root.status)}`);
|
|
131
|
+
|
|
132
|
+
for (const child of root.children ?? []) {
|
|
133
|
+
appendWorkTreeLines(lines, child, epicId, classNames);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push(' classDef question fill:#f4f5f7,stroke:#6554c0,color:#172b4d');
|
|
139
|
+
lines.push(' classDef analysis fill:#f4f5f7,stroke:#5e6c84,color:#172b4d');
|
|
140
|
+
lines.push(' classDef selected fill:#deebff,stroke:#0052cc,color:#172b4d');
|
|
141
|
+
lines.push(' classDef rejected fill:#fafbfc,stroke:#c1c7d0,color:#5e6c84,stroke-dasharray:4 3');
|
|
142
|
+
lines.push(' classDef decision fill:#e3fcef,stroke:#00875a,color:#172b4d');
|
|
143
|
+
lines.push(' classDef done fill:#e3fcef,stroke:#00875a,color:#172b4d');
|
|
144
|
+
lines.push(` ${classNames.join('\n ')}`);
|
|
145
|
+
|
|
146
|
+
const source = lines.join('\n');
|
|
147
|
+
return {
|
|
148
|
+
schema: INTENT_ROADMAP_MERMAID_SCHEMA,
|
|
149
|
+
source,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function enrichIntentRoadmapBranchWithMermaid(branch, intentNodes) {
|
|
154
|
+
const allOptions = findAllOptionsForQuestion(branch.question, intentNodes);
|
|
155
|
+
const enriched = {
|
|
156
|
+
...branch,
|
|
157
|
+
allOptions: allOptions.map((option) => ({
|
|
158
|
+
id: option.id,
|
|
159
|
+
title: option.title,
|
|
160
|
+
selected: option.id === branch.selectedOption?.id || option.selected === true,
|
|
161
|
+
})),
|
|
162
|
+
};
|
|
163
|
+
enriched.mermaid = buildIntentRoadmapMermaidSource(enriched);
|
|
164
|
+
return enriched;
|
|
165
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { attachDerivedWorkItemHierarchy, readWorkItemKind } from './workItemHierarchy.mjs';
|
|
2
|
+
import { buildIntentGraphProjection, resolveIntentBranchForAnalyticsRecord } from './intentGraphProjection.mjs';
|
|
3
|
+
import { enrichIntentRoadmapBranchWithCanvas } from './intentRoadmapCanvas.mjs';
|
|
4
|
+
import { parseCollapsedEpicIds } from './intentRoadmapEpicProjection.mjs';
|
|
5
|
+
|
|
6
|
+
export const INTENT_ROADMAP_PROJECTION_SCHEMA = 'workgraph.intent-roadmap.projection.v1';
|
|
7
|
+
|
|
8
|
+
const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
|
|
9
|
+
const DONE_STATUSES = new Set(['done', 'verified']);
|
|
10
|
+
|
|
11
|
+
function buildRoadmapBranch(decision, intentGraph, workItems) {
|
|
12
|
+
const decisionId = decision.id;
|
|
13
|
+
const roots = workItems.filter((item) => {
|
|
14
|
+
const labels = item.labels ?? {};
|
|
15
|
+
return String(labels['intent.decision_id'] ?? '').trim() === decisionId
|
|
16
|
+
&& readWorkItemParentId(item) === '';
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const buildNode = (item) => {
|
|
20
|
+
const childItems = (item.childIds ?? [])
|
|
21
|
+
.map((childId) => workItems.find((candidate) => candidate.id === childId))
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
|
|
24
|
+
const doneChildCount = childItems.filter((child) => DONE_STATUSES.has(child.status)).length;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
workId: item.id,
|
|
28
|
+
title: item.title ?? item.id,
|
|
29
|
+
status: item.status ?? '',
|
|
30
|
+
parentId: item.parentId ?? '',
|
|
31
|
+
itemKind: readWorkItemKind(item),
|
|
32
|
+
childCount: childItems.length,
|
|
33
|
+
doneChildCount,
|
|
34
|
+
closeBlocked: childItems.length > 0 && doneChildCount < childItems.length,
|
|
35
|
+
children: childItems.map(buildNode).sort((left, right) => compareText(left.workId, right.workId)),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
decisionId,
|
|
41
|
+
decisionTitle: decision.title ?? decisionId,
|
|
42
|
+
analyticsRef: decision.links?.analytics_ref ?? '',
|
|
43
|
+
roots: roots.map(buildNode).sort((left, right) => compareText(left.workId, right.workId)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readWorkItemParentId(item) {
|
|
48
|
+
return String(item.parentId ?? item.labels?.['work.parent_id'] ?? '').trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildIntentRoadmapProjection(intentNodes, workItems, options = {}) {
|
|
52
|
+
const intentGraph = buildIntentGraphProjection(intentNodes, workItems, options);
|
|
53
|
+
const enrichedWorkItems = attachDerivedWorkItemHierarchy(workItems);
|
|
54
|
+
const selectedDecisions = intentGraph.nodes.filter((node) => node.nodeKind === 'decision' && node.selected === true);
|
|
55
|
+
const canvasOptions = {
|
|
56
|
+
collapsedEpicIds: parseCollapsedEpicIds(options.collapsed),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const branches = selectedDecisions.map((decision) =>
|
|
60
|
+
enrichIntentRoadmapBranchWithCanvas(
|
|
61
|
+
buildRoadmapBranch(decision, intentGraph, enrichedWorkItems),
|
|
62
|
+
intentGraph.nodes,
|
|
63
|
+
canvasOptions,
|
|
64
|
+
),
|
|
65
|
+
).sort((left, right) => compareText(left.decisionId, right.decisionId));
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
schema: INTENT_ROADMAP_PROJECTION_SCHEMA,
|
|
69
|
+
readOnly: true,
|
|
70
|
+
branchCount: branches.length,
|
|
71
|
+
branches,
|
|
72
|
+
intentGraphSchema: intentGraph.schema,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildIntentRoadmapForAnalyticsRecord(record, intentNodes, workItems) {
|
|
77
|
+
const intentGraph = buildIntentGraphProjection(intentNodes, workItems);
|
|
78
|
+
const branch = resolveIntentBranchForAnalyticsRecord(record, intentGraph);
|
|
79
|
+
if (!branch?.selectedDecision) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const projection = buildIntentRoadmapProjection(intentNodes, workItems);
|
|
84
|
+
return projection.branches.find((entry) => entry.decisionId === branch.selectedDecision.id) ?? null;
|
|
85
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { access, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, posix } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { parseIntentIndexEntries } from './intentTreeWorkItems.mjs';
|
|
5
|
+
import { isWorkItemArtifactPath, workItemPathMatchesId } from './bvcNewWritePolicy.mjs';
|
|
6
|
+
|
|
7
|
+
export const INTENT_TREE_LINT_SCHEMA = 'workgraph.intent-tree.lint.v1';
|
|
8
|
+
|
|
9
|
+
async function fileExists(absolutePath) {
|
|
10
|
+
try {
|
|
11
|
+
await access(absolutePath);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function listIntentWorkStepFiles(rootDir, cwd) {
|
|
19
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
20
|
+
const nested = await Promise.all(
|
|
21
|
+
entries.map(async (entry) => {
|
|
22
|
+
const entryPath = join(rootDir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
return listIntentWorkStepFiles(entryPath, cwd);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return entry.isFile() && isWorkItemArtifactPath(entry.name) ? [entryPath] : [];
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return nested.flat();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toRepoRelativePath(cwd, absolutePath) {
|
|
35
|
+
return absolutePath
|
|
36
|
+
.slice(cwd.length + 1)
|
|
37
|
+
.split(/\\/u)
|
|
38
|
+
.join('/');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {{ cwd?: string, intentRoot?: string, indexPath?: string, indexText?: string }} [options]
|
|
43
|
+
*/
|
|
44
|
+
export async function lintIntentTreeOrphans(options = {}) {
|
|
45
|
+
const cwd = options.cwd ?? process.cwd();
|
|
46
|
+
const intentRoot = join(cwd, options.intentRoot ?? 'intent');
|
|
47
|
+
const indexPath = options.indexPath ?? 'intent/index.bvc';
|
|
48
|
+
const { readFile } = await import('node:fs/promises');
|
|
49
|
+
const indexText = options.indexText ?? await readFile(join(cwd, indexPath), 'utf8');
|
|
50
|
+
const indexEntries = parseIntentIndexEntries(indexText);
|
|
51
|
+
const indexById = new Map(indexEntries.map((entry) => [entry.id, entry.path]));
|
|
52
|
+
const indexPaths = new Set(indexEntries.map((entry) => entry.path.replace(/\\/g, '/')));
|
|
53
|
+
|
|
54
|
+
const diskFiles = await listIntentWorkStepFiles(intentRoot, cwd);
|
|
55
|
+
const diskPaths = diskFiles.map((filePath) => toRepoRelativePath(cwd, filePath));
|
|
56
|
+
|
|
57
|
+
const orphanFiles = diskPaths
|
|
58
|
+
.filter((relativePath) => !indexPaths.has(relativePath))
|
|
59
|
+
.sort()
|
|
60
|
+
.map((path) => ({ path, kind: 'orphan_file' }));
|
|
61
|
+
|
|
62
|
+
const missingFiles = [];
|
|
63
|
+
const workIdMismatches = [];
|
|
64
|
+
|
|
65
|
+
for (const entry of indexEntries) {
|
|
66
|
+
const normalizedPath = entry.path.replace(/\\/g, '/');
|
|
67
|
+
const absolutePath = join(cwd, normalizedPath);
|
|
68
|
+
const exists = await fileExists(absolutePath);
|
|
69
|
+
|
|
70
|
+
if (!exists) {
|
|
71
|
+
missingFiles.push({
|
|
72
|
+
workId: entry.id,
|
|
73
|
+
path: normalizedPath,
|
|
74
|
+
kind: 'missing_file',
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!workItemPathMatchesId(entry.id, normalizedPath)) {
|
|
80
|
+
workIdMismatches.push({
|
|
81
|
+
workId: entry.id,
|
|
82
|
+
path: normalizedPath,
|
|
83
|
+
kind: 'path_work_id_mismatch',
|
|
84
|
+
expectedSuffix: `${entry.id}.work.bvc (or legacy .work.bvc)`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const indexedIds = new Set(indexEntries.map((entry) => entry.id));
|
|
90
|
+
const duplicateIds = indexEntries
|
|
91
|
+
.map((entry) => entry.id)
|
|
92
|
+
.filter((id, index, array) => array.indexOf(id) !== index);
|
|
93
|
+
|
|
94
|
+
const errors = [
|
|
95
|
+
...orphanFiles.map((row) => `orphan work item file without index entry: ${row.path}`),
|
|
96
|
+
...missingFiles.map((row) => `index entry without file: ${row.workId} -> ${row.path}`),
|
|
97
|
+
...workIdMismatches.map((row) => `index path mismatch for ${row.workId}: ${row.path}`),
|
|
98
|
+
...duplicateIds.map((id) => `duplicate index work.id: ${id}`),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
schema: INTENT_TREE_LINT_SCHEMA,
|
|
103
|
+
ok: errors.length === 0,
|
|
104
|
+
indexPath,
|
|
105
|
+
indexedCount: indexEntries.length,
|
|
106
|
+
diskFileCount: diskPaths.length,
|
|
107
|
+
orphanFiles,
|
|
108
|
+
missingFiles,
|
|
109
|
+
workIdMismatches,
|
|
110
|
+
duplicateIndexIds: [...new Set(duplicateIds)].sort(),
|
|
111
|
+
errors,
|
|
112
|
+
indexById: Object.fromEntries(indexById),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, posix } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { buildSnapshot, parseWorkItems } from './workGraphRuntime.mjs';
|
|
6
|
+
import { classifyIntentFolder, toIntentPath } from './intentHierarchy.mjs';
|
|
7
|
+
|
|
8
|
+
const STEP_ATOM_PATTERN = /^#([^\n<]+)<\[\n([\s\S]*?)\n\]>/gmu;
|
|
9
|
+
|
|
10
|
+
export function extractWorkItemAtoms(backlogText) {
|
|
11
|
+
if (typeof backlogText !== 'string') {
|
|
12
|
+
throw new TypeError('backlogText must be a string');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return [...backlogText.matchAll(STEP_ATOM_PATTERN)]
|
|
16
|
+
.map((match) => {
|
|
17
|
+
const [content] = match;
|
|
18
|
+
const [item] = parseWorkItems(content);
|
|
19
|
+
return item ? { item, content: ensureTrailingNewline(content) } : null;
|
|
20
|
+
})
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildIntentTreeEntries(backlogText) {
|
|
25
|
+
const entries = extractWorkItemAtoms(backlogText).map(({ item, content }) => ({
|
|
26
|
+
id: item.id,
|
|
27
|
+
path: toIntentPathFromItem(item),
|
|
28
|
+
content,
|
|
29
|
+
item,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
assertUniquePaths(entries);
|
|
33
|
+
return entries.sort((left, right) => compareText(left.id, right.id));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildIntentIndexStep(entries) {
|
|
37
|
+
const lines = entries
|
|
38
|
+
.map((entry) => ` - ${entry.id}: ${toPortablePath(entry.path)}`)
|
|
39
|
+
.join('\n');
|
|
40
|
+
|
|
41
|
+
return `#Индекс_Intent_Tree_WorkItems<[
|
|
42
|
+
Базис:
|
|
43
|
+
Generated compatibility manifest for WorkItem atoms split from work/backlog.bvc.
|
|
44
|
+
Вектор:
|
|
45
|
+
Keep work/backlog.bvc intact for current runtime/UI readers until they can read intent/**/*.work.bvc directly.
|
|
46
|
+
Цель:
|
|
47
|
+
Provide a deterministic inventory of canonical intent tree task files without changing current backlog compatibility.
|
|
48
|
+
WorkItems:
|
|
49
|
+
${lines}
|
|
50
|
+
|
|
51
|
+
Метки:
|
|
52
|
+
atom.profile: trace
|
|
53
|
+
intent.index: work_items
|
|
54
|
+
intent.source: work/backlog.bvc
|
|
55
|
+
intent.compatibility_projection: work/backlog.bvc
|
|
56
|
+
trace.status: pending
|
|
57
|
+
]>
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function compareBacklogAndIntentSnapshots(backlogText, intentFileTexts) {
|
|
62
|
+
const backlogSnapshot = buildSnapshot(parseWorkItems(backlogText));
|
|
63
|
+
const intentSnapshot = buildSnapshot(parseWorkItems(intentFileTexts.join('\n')));
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
equal: JSON.stringify(backlogSnapshot) === JSON.stringify(intentSnapshot),
|
|
67
|
+
backlogSnapshot,
|
|
68
|
+
intentSnapshot,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function writeIntentTreeFromBacklog(backlogText, options = {}) {
|
|
73
|
+
const root = options.root ?? process.cwd();
|
|
74
|
+
if (options.clean === true) {
|
|
75
|
+
await rm(join(root, 'intent'), { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const entries = buildIntentTreeEntries(backlogText);
|
|
79
|
+
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
const absolutePath = join(root, entry.path);
|
|
82
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
83
|
+
await writeFile(absolutePath, entry.content, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const indexPath = options.indexPath ?? 'intent/index.bvc';
|
|
87
|
+
await mkdir(dirname(join(root, indexPath)), { recursive: true });
|
|
88
|
+
await writeFile(join(root, indexPath), buildIntentIndexStep(entries), 'utf8');
|
|
89
|
+
|
|
90
|
+
const comparison = compareBacklogAndIntentSnapshots(
|
|
91
|
+
backlogText,
|
|
92
|
+
entries.map((entry) => entry.content),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
entries,
|
|
97
|
+
indexPath,
|
|
98
|
+
comparison,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toIntentPathFromItem(item) {
|
|
103
|
+
return toIntentPath(item);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function assertUniquePaths(entries) {
|
|
107
|
+
const seen = new Map();
|
|
108
|
+
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const existing = seen.get(entry.path);
|
|
111
|
+
if (existing !== undefined) {
|
|
112
|
+
throw new Error(`duplicate intent path ${entry.path} for ${existing} and ${entry.id}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
seen.set(entry.path, entry.id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toPortablePath(path) {
|
|
120
|
+
return path;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ensureTrailingNewline(text) {
|
|
124
|
+
return text.endsWith('\n') ? text : `${text}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compareText(left, right) {
|
|
128
|
+
return left.localeCompare(right, 'en', { sensitivity: 'variant' });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { lintIntentTreeOrphans } from './intentTreeLint.mjs';
|
|
132
|
+
|
|
133
|
+
async function main() {
|
|
134
|
+
const { readFile } = await import('node:fs/promises');
|
|
135
|
+
const root = process.cwd();
|
|
136
|
+
const backlogPath = join(root, 'work/backlog.bvc');
|
|
137
|
+
const backlogText = await readFile(backlogPath, 'utf8');
|
|
138
|
+
const result = await writeIntentTreeFromBacklog(backlogText, { root, clean: true });
|
|
139
|
+
|
|
140
|
+
if (!result.comparison.equal) {
|
|
141
|
+
throw new Error('intent tree split did not preserve backlog snapshot');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`Wrote ${result.entries.length} WorkItem files and ${result.indexPath}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const invokedUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : '';
|
|
148
|
+
if (invokedUrl === import.meta.url) {
|
|
149
|
+
await main();
|
|
150
|
+
}
|