@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,100 @@
|
|
|
1
|
+
import { buildSnapshot, parseWorkItems } from './workGraphRuntime.mjs';
|
|
2
|
+
import { buildVerificationSummary } from './verificationLoop.mjs';
|
|
3
|
+
import { executeOnebaseVerificationCommand } from './onebaseWorkerTools.mjs';
|
|
4
|
+
|
|
5
|
+
export const BLOCKED_ONEBASE_GO_PREFLIGHT_EVAL_SCHEMA = 'workgraph.blocked-onebase-go-preflight.eval.v1';
|
|
6
|
+
|
|
7
|
+
export const BLOCKED_ONEBASE_GO_BACKLOG = `#Задача_blocked_onebase<[
|
|
8
|
+
Базис:
|
|
9
|
+
OneBase blocked by missing Go toolchain.
|
|
10
|
+
Вектор:
|
|
11
|
+
Record blocked evidence instead of failed verify.
|
|
12
|
+
Цель:
|
|
13
|
+
Optional env gate.
|
|
14
|
+
|
|
15
|
+
Свидетельства:
|
|
16
|
+
Blocker evidence: go version CommandNotFoundException; go not found in PATH; blocked evidence recorded.
|
|
17
|
+
|
|
18
|
+
Метки:
|
|
19
|
+
atom.profile: work_item
|
|
20
|
+
work.id: onebase-implement-gross-profit-warehouse-dimension
|
|
21
|
+
work.title: OneBase gross profit warehouse
|
|
22
|
+
work.status: blocked
|
|
23
|
+
work.blocker: go not in PATH
|
|
24
|
+
trace.status: verified
|
|
25
|
+
]>
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export function evaluateBlockedOnebaseGoPreflightDeterministic(options = {}) {
|
|
29
|
+
const items = parseWorkItems(options.backlogText ?? BLOCKED_ONEBASE_GO_BACKLOG);
|
|
30
|
+
const snapshot = buildSnapshot(items);
|
|
31
|
+
const summary = buildVerificationSummary(snapshot, { items });
|
|
32
|
+
const onebaseRow = summary.matrix.find((row) => row.id === 'onebase-go-test');
|
|
33
|
+
const blockedTask = items.find((item) => item.id === 'onebase-implement-gross-profit-warehouse-dimension');
|
|
34
|
+
const combined = JSON.stringify({ summary, onebaseRow, blockedTask });
|
|
35
|
+
|
|
36
|
+
const keywordsOk = ['blocked', 'go version', 'preflight'].every((keyword) => {
|
|
37
|
+
if (keyword === 'preflight') {
|
|
38
|
+
return combined.includes('preflightCommand') || combined.toLowerCase().includes('preflight');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return combined.toLowerCase().includes(keyword.toLowerCase());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
ok: onebaseRow?.status === 'blocked'
|
|
46
|
+
&& summary.onebaseGate?.status === 'blocked'
|
|
47
|
+
&& blockedTask?.status === 'blocked'
|
|
48
|
+
&& keywordsOk,
|
|
49
|
+
onebaseRowStatus: onebaseRow?.status ?? null,
|
|
50
|
+
onebaseGateStatus: summary.onebaseGate?.status ?? null,
|
|
51
|
+
blockedTaskStatus: blockedTask?.status ?? null,
|
|
52
|
+
preflightCommand: summary.onebaseGate?.preflightCommand ?? null,
|
|
53
|
+
keywordsOk,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function evaluateOnebaseVerificationCommandGoPreflight(options = {}) {
|
|
58
|
+
const spawnSyncImpl = options.spawnSyncImpl ?? (() => ({
|
|
59
|
+
status: 1,
|
|
60
|
+
stdout: '',
|
|
61
|
+
stderr: 'go not found in PATH',
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const result = executeOnebaseVerificationCommand({
|
|
65
|
+
policy: { allowShell: true },
|
|
66
|
+
onebaseRoot: options.onebaseRoot ?? '../onebase',
|
|
67
|
+
spawnSyncImpl,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const combined = JSON.stringify(result);
|
|
71
|
+
const keywordsOk = ['go version', 'preflight'].every((keyword) => combined.toLowerCase().includes(keyword));
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ok: result.ok === false
|
|
75
|
+
&& result.blocked !== true
|
|
76
|
+
&& result.reason === 'go preflight failed'
|
|
77
|
+
&& keywordsOk,
|
|
78
|
+
result,
|
|
79
|
+
keywordsOk,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function runBlockedOnebaseGoPreflightEval(options = {}) {
|
|
84
|
+
const deterministic = evaluateBlockedOnebaseGoPreflightDeterministic(options);
|
|
85
|
+
const verificationCommand = evaluateOnebaseVerificationCommandGoPreflight(options);
|
|
86
|
+
|
|
87
|
+
const ok = deterministic.ok && verificationCommand.ok;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
schema: BLOCKED_ONEBASE_GO_PREFLIGHT_EVAL_SCHEMA,
|
|
91
|
+
ok,
|
|
92
|
+
failureClass: ok ? null : 'code_failure',
|
|
93
|
+
reason: ok
|
|
94
|
+
? 'OneBase go preflight blocked path verified deterministically (no live LLM required)'
|
|
95
|
+
: 'blocked-onebase-go-preflight deterministic checks failed',
|
|
96
|
+
deterministic,
|
|
97
|
+
verificationCommand,
|
|
98
|
+
live: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const BRACKET_IR_PARSER_ENGINE_VERSION = 'bracket-ir-engine-v1-port-ref';
|
|
4
|
+
|
|
5
|
+
const BRACKET_PREFIX = /^bracket:\s*/iu;
|
|
6
|
+
|
|
7
|
+
export function normalizeBracketIrTextForHash(text) {
|
|
8
|
+
let normalized = String(text ?? '').replace(/\r\n/gu, '\n').replace(/\r/gu, '\n');
|
|
9
|
+
normalized = normalized.replace(/[ \t\u00a0]+$/gmu, '');
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function extractBracketBodyForHash(vectorRaw) {
|
|
14
|
+
const trimmed = String(vectorRaw ?? '').trimStart();
|
|
15
|
+
const body = trimmed.replace(BRACKET_PREFIX, '');
|
|
16
|
+
if (trimmed === body) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return body;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function computeBracketIrVectorHash(bracketBody, options = {}) {
|
|
24
|
+
const normalizedBody = normalizeBracketIrTextForHash(bracketBody);
|
|
25
|
+
const engineVersion = options.engineVersion ?? BRACKET_IR_PARSER_ENGINE_VERSION;
|
|
26
|
+
const vectorHash = createHash('sha256').update(`${normalizedBody}\0${engineVersion}`, 'utf8').digest('hex');
|
|
27
|
+
|
|
28
|
+
return { vectorHash, normalizedBody, engineVersion };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function extractBracketVectorFromStepText(stepText) {
|
|
32
|
+
const vectorSection = /(?:^|\n)Вектор:\s*\n([\s\S]*?)(?=\n(?:Цель:|Метки:|критерии_готовности:|Проверки:)|\n#|\s*$)/u.exec(String(stepText ?? ''));
|
|
33
|
+
if (vectorSection === null) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return extractBracketBodyForHash(vectorSection[1]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildBracketIrTraceSignal(item, options = {}) {
|
|
41
|
+
const storedHash = String(item.labels?.['trace.bracket_ir_hash'] ?? '').trim();
|
|
42
|
+
const stepPath = String(item.labels?.['trace.bracket_ir_step'] ?? item.labels?.['trace.source_step'] ?? '').trim();
|
|
43
|
+
const stepText = options.stepTextByPath?.[stepPath] ?? options.stepText ?? '';
|
|
44
|
+
const bracketBody = extractBracketVectorFromStepText(stepText);
|
|
45
|
+
|
|
46
|
+
if (bracketBody === null) {
|
|
47
|
+
return {
|
|
48
|
+
schema: 'bracket-ir.trace-signal.v1',
|
|
49
|
+
workId: item.id,
|
|
50
|
+
stepPath,
|
|
51
|
+
hasBracketSection: false,
|
|
52
|
+
storedHash,
|
|
53
|
+
currentHash: '',
|
|
54
|
+
drift: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { vectorHash } = computeBracketIrVectorHash(bracketBody, options);
|
|
59
|
+
const drift = storedHash !== '' && storedHash !== vectorHash;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
schema: 'bracket-ir.trace-signal.v1',
|
|
63
|
+
workId: item.id,
|
|
64
|
+
stepPath,
|
|
65
|
+
hasBracketSection: true,
|
|
66
|
+
storedHash,
|
|
67
|
+
currentHash: vectorHash,
|
|
68
|
+
engineVersion: options.engineVersion ?? BRACKET_IR_PARSER_ENGINE_VERSION,
|
|
69
|
+
drift,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function evaluateBracketIrDrift(item, options = {}) {
|
|
74
|
+
const signal = buildBracketIrTraceSignal(item, options);
|
|
75
|
+
if (!signal.hasBracketSection || !signal.storedHash) {
|
|
76
|
+
return { ok: true, signal, diagnostics: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!signal.drift) {
|
|
80
|
+
return { ok: true, signal, diagnostics: [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
signal,
|
|
86
|
+
diagnostics: [{
|
|
87
|
+
severity: 'error',
|
|
88
|
+
code: 'bracket_ir.hash_drift',
|
|
89
|
+
message: `WorkItem ${item.id} Bracket IR hash drift detected for ${signal.stepPath || 'unknown step'}.`,
|
|
90
|
+
actionable: 'Re-run compiler round-trip or update trace.bracket_ir_hash after reviewed step edit.',
|
|
91
|
+
}],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectDialectFromBvcSectionTitle,
|
|
3
|
+
getDialect,
|
|
4
|
+
normalizeDialectId,
|
|
5
|
+
parseBvcFilePragma,
|
|
6
|
+
REGISTERED_DIALECT_IDS,
|
|
7
|
+
} from './bvcDialectRegistry.mjs';
|
|
8
|
+
|
|
9
|
+
export const BVC_LINT_E_DIALECT_MIX = 'E_BVC_DIALECT_MIX';
|
|
10
|
+
export const BVC_LINT_E_DIALECT_LANG_MISMATCH = 'E_BVC_DIALECT_LANG_MISMATCH';
|
|
11
|
+
export const BVC_LINT_W_LANG_FALLBACK_EN = 'W_BVC_LANG_FALLBACK_EN';
|
|
12
|
+
|
|
13
|
+
export const BVC_ATOM_AST_SCHEMA = 'bvc-atom.ast.v1';
|
|
14
|
+
export const BVC_DOCUMENT_AST_SCHEMA = 'bvc-document.ast.v1';
|
|
15
|
+
|
|
16
|
+
/** @param {string} text */
|
|
17
|
+
export function stripBvcFilePragmaLine(text) {
|
|
18
|
+
const lines = text.split(/\r?\n/u);
|
|
19
|
+
if (lines.length === 0) {
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
if (parseBvcFilePragma(text) !== null) {
|
|
23
|
+
return lines.slice(1).join('\n');
|
|
24
|
+
}
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} rawHeader
|
|
30
|
+
* @returns {{ name: string, headerLang: string | null }}
|
|
31
|
+
*/
|
|
32
|
+
export function parseBvcAtomHeader(rawHeader) {
|
|
33
|
+
const trimmed = String(rawHeader ?? '').trim();
|
|
34
|
+
const atIndex = trimmed.lastIndexOf('@');
|
|
35
|
+
if (atIndex === -1) {
|
|
36
|
+
return { name: trimmed, headerLang: null };
|
|
37
|
+
}
|
|
38
|
+
const name = trimmed.slice(0, atIndex).trim();
|
|
39
|
+
const langToken = trimmed.slice(atIndex + 1).trim().toLowerCase();
|
|
40
|
+
if (!REGISTERED_DIALECT_IDS.includes(langToken)) {
|
|
41
|
+
return { name: trimmed, headerLang: null };
|
|
42
|
+
}
|
|
43
|
+
return { name, headerLang: langToken };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} body
|
|
48
|
+
* @returns {{ dialects: Set<string>, firstBvcDialect: string | null }}
|
|
49
|
+
*/
|
|
50
|
+
export function scanBvcSectionDialects(body) {
|
|
51
|
+
/** @type {Set<string>} */
|
|
52
|
+
const dialects = new Set();
|
|
53
|
+
let firstBvcDialect = null;
|
|
54
|
+
|
|
55
|
+
for (const rawLine of body.split(/\r?\n/u)) {
|
|
56
|
+
const line = rawLine.trim();
|
|
57
|
+
if (!line.endsWith(':')) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const title = line.slice(0, -1);
|
|
61
|
+
const detected = detectDialectFromBvcSectionTitle(title);
|
|
62
|
+
if (detected === null) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
dialects.add(detected);
|
|
66
|
+
if (firstBvcDialect === null) {
|
|
67
|
+
firstBvcDialect = detected;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { dialects, firstBvcDialect };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {{
|
|
76
|
+
* headerLang?: string | null,
|
|
77
|
+
* labelsLang?: string | null,
|
|
78
|
+
* filePragmaLang?: string | null,
|
|
79
|
+
* autoDetectLang?: string | null,
|
|
80
|
+
* }} sources
|
|
81
|
+
*/
|
|
82
|
+
export function resolveAtomLang(sources) {
|
|
83
|
+
const warnings = [];
|
|
84
|
+
const headerLang = sources.headerLang ? normalizeDialectId(sources.headerLang) : null;
|
|
85
|
+
const labelsLang = sources.labelsLang ? normalizeDialectId(sources.labelsLang) : null;
|
|
86
|
+
const filePragmaLang = sources.filePragmaLang ? normalizeDialectId(sources.filePragmaLang) : null;
|
|
87
|
+
const autoDetectLang = sources.autoDetectLang ? normalizeDialectId(sources.autoDetectLang) : null;
|
|
88
|
+
|
|
89
|
+
if (headerLang !== null) {
|
|
90
|
+
return { lang: headerLang, source: 'atom_header', warnings };
|
|
91
|
+
}
|
|
92
|
+
if (labelsLang !== null) {
|
|
93
|
+
return { lang: labelsLang, source: 'labels.lang', warnings };
|
|
94
|
+
}
|
|
95
|
+
if (filePragmaLang !== null) {
|
|
96
|
+
return { lang: filePragmaLang, source: 'file_pragma', warnings };
|
|
97
|
+
}
|
|
98
|
+
if (autoDetectLang !== null) {
|
|
99
|
+
return { lang: autoDetectLang, source: 'auto_detect', warnings };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
warnings.push(BVC_LINT_W_LANG_FALLBACK_EN);
|
|
103
|
+
return { lang: 'en', source: 'fallback_en', warnings };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} body
|
|
108
|
+
* @param {string} resolvedLang
|
|
109
|
+
*/
|
|
110
|
+
export function lintBvcAtomDialect(body, resolvedLang) {
|
|
111
|
+
/** @type {Array<{ code: string, message: string }>} */
|
|
112
|
+
const lints = [];
|
|
113
|
+
const { dialects } = scanBvcSectionDialects(body);
|
|
114
|
+
|
|
115
|
+
if (dialects.size > 1) {
|
|
116
|
+
lints.push({
|
|
117
|
+
code: BVC_LINT_E_DIALECT_MIX,
|
|
118
|
+
message: `Mixed BVC dialect keys in one atom: ${Array.from(dialects).join(', ')}`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (dialects.size === 1) {
|
|
123
|
+
const [onlyDialect] = dialects;
|
|
124
|
+
if (onlyDialect !== resolvedLang) {
|
|
125
|
+
lints.push({
|
|
126
|
+
code: BVC_LINT_E_DIALECT_LANG_MISMATCH,
|
|
127
|
+
message: `Resolved lang=${resolvedLang} but BVC keys use dialect ${onlyDialect}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return lints;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {import('./stepAtomFormatter.mjs').StepAtomDraftLike} draft
|
|
137
|
+
* @param {string} lang
|
|
138
|
+
* @param {Array<{ code: string, message: string }>} lints
|
|
139
|
+
*/
|
|
140
|
+
export function buildBvcAtomAst(draft, lang, lints = []) {
|
|
141
|
+
return {
|
|
142
|
+
schema: BVC_ATOM_AST_SCHEMA,
|
|
143
|
+
name: draft.name,
|
|
144
|
+
lang,
|
|
145
|
+
bvc: {
|
|
146
|
+
basis: [...(draft.basis ?? [])],
|
|
147
|
+
vector: [...(draft.vector ?? [])],
|
|
148
|
+
goal: [...(draft.goal ?? [])],
|
|
149
|
+
},
|
|
150
|
+
labels: { ...(draft.labels ?? {}) },
|
|
151
|
+
optional: {
|
|
152
|
+
checks: draft.checks ? [...draft.checks] : [],
|
|
153
|
+
evidence: draft.evidence ? [...draft.evidence] : [],
|
|
154
|
+
analysis: draft.analysis ? [...draft.analysis] : [],
|
|
155
|
+
decision: draft.decision ? [...draft.decision] : [],
|
|
156
|
+
uiRefs: draft.uiRefs ? [...draft.uiRefs] : [],
|
|
157
|
+
},
|
|
158
|
+
profile: draft.profile ?? draft.labels?.['atom.profile'] ?? '',
|
|
159
|
+
lints,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} text
|
|
165
|
+
*/
|
|
166
|
+
export function parseBvcDocument(text) {
|
|
167
|
+
if (typeof text !== 'string') {
|
|
168
|
+
throw new TypeError('text must be a string');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const fileLang = parseBvcFilePragma(text);
|
|
172
|
+
const bodyText = stripBvcFilePragmaLine(text);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
schema: BVC_DOCUMENT_AST_SCHEMA,
|
|
176
|
+
fileLang,
|
|
177
|
+
bodyText,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** @param {string} name @param {string} [lang] */
|
|
182
|
+
export function formatBvcAtomHeader(name, lang) {
|
|
183
|
+
const trimmed = String(name ?? '').trim();
|
|
184
|
+
if (lang === 'en') {
|
|
185
|
+
return `#${trimmed}@en<[` ;
|
|
186
|
+
}
|
|
187
|
+
return `#${trimmed}<[` ;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Compare normalized BVC content (EN-canonical arrays) for conformance.
|
|
192
|
+
* @param {ReturnType<typeof buildBvcAtomAst>} left
|
|
193
|
+
* @param {ReturnType<typeof buildBvcAtomAst>} right
|
|
194
|
+
*/
|
|
195
|
+
export function bvcAtomAstEquivalent(left, right) {
|
|
196
|
+
return JSON.stringify({
|
|
197
|
+
bvc: left.bvc,
|
|
198
|
+
profile: left.profile,
|
|
199
|
+
labels: left.labels,
|
|
200
|
+
}) === JSON.stringify({
|
|
201
|
+
bvc: right.bvc,
|
|
202
|
+
profile: right.profile,
|
|
203
|
+
labels: right.labels,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @param {string} lang @param {string} field */
|
|
208
|
+
export function localizedBvcTitle(lang, field) {
|
|
209
|
+
return getDialect(lang).bvc[field];
|
|
210
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import enDialect from '../packages/bvc-dialects/en.json' with { type: 'json' };
|
|
2
|
+
import ruDialect from '../packages/bvc-dialects/ru.json' with { type: 'json' };
|
|
3
|
+
|
|
4
|
+
/** @type {Record<string, import('./bvcDialectRegistry.types').BvcDialectV1>} */
|
|
5
|
+
const DIALECTS = {
|
|
6
|
+
en: enDialect,
|
|
7
|
+
ru: ruDialect,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const REGISTERED_DIALECT_IDS = Object.freeze(Object.keys(DIALECTS));
|
|
11
|
+
|
|
12
|
+
const BVC_FIELDS = ['basis', 'vector', 'goal', 'labels'];
|
|
13
|
+
const OPTIONAL_FIELDS = ['checks', 'evidence', 'analysis', 'decision', 'uiRefs'];
|
|
14
|
+
const ALL_SECTION_FIELDS = [...BVC_FIELDS, ...OPTIONAL_FIELDS];
|
|
15
|
+
|
|
16
|
+
/** @param {string} [lang] */
|
|
17
|
+
export function normalizeDialectId(lang) {
|
|
18
|
+
const id = String(lang ?? '').trim().toLowerCase();
|
|
19
|
+
if (id === '') {
|
|
20
|
+
return 'ru';
|
|
21
|
+
}
|
|
22
|
+
if (!Object.hasOwn(DIALECTS, id)) {
|
|
23
|
+
throw new Error(`Unknown BVC dialect: ${lang}`);
|
|
24
|
+
}
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {string} [lang] */
|
|
29
|
+
export function getDialect(lang) {
|
|
30
|
+
return DIALECTS[normalizeDialectId(lang)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {string} [lang] */
|
|
34
|
+
export function getSectionTitle(lang, field) {
|
|
35
|
+
const dialect = getDialect(lang);
|
|
36
|
+
if (BVC_FIELDS.includes(field)) {
|
|
37
|
+
return dialect.bvc[field];
|
|
38
|
+
}
|
|
39
|
+
if (OPTIONAL_FIELDS.includes(field)) {
|
|
40
|
+
return dialect.optional?.[field] ?? DIALECTS.ru.optional[field];
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Unknown BVC section field: ${field}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {string} [lang] */
|
|
46
|
+
export function getFieldSectionsForDialect(lang) {
|
|
47
|
+
return ALL_SECTION_FIELDS.map((field) => [field, getSectionTitle(lang, field)]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildSectionTitleToFieldMap() {
|
|
51
|
+
/** @type {Map<string, string>} */
|
|
52
|
+
const map = new Map();
|
|
53
|
+
for (const dialectId of REGISTERED_DIALECT_IDS) {
|
|
54
|
+
for (const [field, title] of getFieldSectionsForDialect(dialectId)) {
|
|
55
|
+
map.set(title, field);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
map.set('критерии_готовности', 'checks');
|
|
59
|
+
return map;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} title
|
|
64
|
+
* @returns {'en' | 'ru' | null}
|
|
65
|
+
*/
|
|
66
|
+
export function detectDialectFromBvcSectionTitle(title) {
|
|
67
|
+
for (const dialectId of REGISTERED_DIALECT_IDS) {
|
|
68
|
+
const dialect = DIALECTS[dialectId];
|
|
69
|
+
if (Object.values(dialect.bvc).includes(title)) {
|
|
70
|
+
return /** @type {'en' | 'ru'} */ (dialectId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const FILE_PRAGMA_PATTERN = /^#!bvc\s+lang=([a-z]{2})\s*$/u;
|
|
77
|
+
|
|
78
|
+
/** @param {string} text */
|
|
79
|
+
export function parseBvcFilePragma(text) {
|
|
80
|
+
const firstLine = text.split(/\r?\n/u)[0]?.trim() ?? '';
|
|
81
|
+
const match = firstLine.match(FILE_PRAGMA_PATTERN);
|
|
82
|
+
if (!match) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return normalizeDialectId(match[1]);
|
|
86
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { parseBvcDocument } from './bvcAtomParser.mjs';
|
|
2
|
+
import { formatStepAtomDraft, parseStepAtomDrafts } from './stepAtomFormatter.mjs';
|
|
3
|
+
import { extensionFromPath } from './languageAdapters/shared.mjs';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const BVC_EXTENSION_CANON = '.bvc';
|
|
9
|
+
export const BVC_EXTENSION_LEGACY = '.step';
|
|
10
|
+
export const BVC_READ_EXTENSIONS = Object.freeze([BVC_EXTENSION_CANON, BVC_EXTENSION_LEGACY]);
|
|
11
|
+
export const BVC_LINT_W_LEGACY_EXTENSION = 'W_BVC_LEGACY_STEP_EXTENSION';
|
|
12
|
+
export const BVC_LEGACY_STEP_READ_WARNING = 'W_BVC_LEGACY_STEP_READ';
|
|
13
|
+
|
|
14
|
+
/** @type {Set<string>} */
|
|
15
|
+
const legacyStepReadWarnings = new Set();
|
|
16
|
+
|
|
17
|
+
/** Reset once-per-path legacy read warnings (tests only). */
|
|
18
|
+
export function resetLegacyStepReadWarningsForTests() {
|
|
19
|
+
legacyStepReadWarnings.clear();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} resolvedPath
|
|
24
|
+
* @param {{ onLegacyStepRead?: (detail: { resolvedPath: string, message: string }) => void }} [options]
|
|
25
|
+
*/
|
|
26
|
+
export function warnLegacyStepRead(resolvedPath, options = {}) {
|
|
27
|
+
if (!isLegacyStepPath(resolvedPath)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const key = resolve(resolvedPath);
|
|
32
|
+
if (legacyStepReadWarnings.has(key)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
legacyStepReadWarnings.add(key);
|
|
36
|
+
|
|
37
|
+
const message = `Legacy ${BVC_EXTENSION_LEGACY} read: ${key}; prefer ${BVC_EXTENSION_CANON} for new writes (deprecation until v2).`;
|
|
38
|
+
if (typeof options.onLegacyStepRead === 'function') {
|
|
39
|
+
options.onLegacyStepRead({ resolvedPath: key, message });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.warn(`[bvc] ${message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {string} filePath */
|
|
46
|
+
export function bvcExtensionFromPath(filePath) {
|
|
47
|
+
return extensionFromPath(filePath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {string} filePath */
|
|
51
|
+
export function isBvcReadablePath(filePath) {
|
|
52
|
+
return BVC_READ_EXTENSIONS.includes(bvcExtensionFromPath(filePath));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {string} filePath */
|
|
56
|
+
export function isLegacyStepPath(filePath) {
|
|
57
|
+
return bvcExtensionFromPath(filePath) === BVC_EXTENSION_LEGACY;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} filePath
|
|
62
|
+
* @param {{ preferCanon?: boolean }} [options]
|
|
63
|
+
*/
|
|
64
|
+
export function swapBvcExtension(filePath, options = {}) {
|
|
65
|
+
const preferCanon = options.preferCanon !== false;
|
|
66
|
+
const ext = bvcExtensionFromPath(filePath);
|
|
67
|
+
if (preferCanon && ext === BVC_EXTENSION_LEGACY) {
|
|
68
|
+
return `${filePath.slice(0, -BVC_EXTENSION_LEGACY.length)}${BVC_EXTENSION_CANON}`;
|
|
69
|
+
}
|
|
70
|
+
if (!preferCanon && ext === BVC_EXTENSION_CANON) {
|
|
71
|
+
return `${filePath.slice(0, -BVC_EXTENSION_CANON.length)}${BVC_EXTENSION_LEGACY}`;
|
|
72
|
+
}
|
|
73
|
+
return filePath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve on-disk path for dual-read: try canonical `.bvc` when legacy `.step` is missing (and vice versa).
|
|
78
|
+
* @param {string} filePath
|
|
79
|
+
* @param {string} [cwd]
|
|
80
|
+
*/
|
|
81
|
+
export function resolveBvcReadablePath(filePath, cwd = process.cwd()) {
|
|
82
|
+
const absolute = resolve(cwd, filePath);
|
|
83
|
+
if (existsSync(absolute)) {
|
|
84
|
+
return absolute;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isBvcReadablePath(absolute)) {
|
|
88
|
+
return absolute;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const preferCanon = isLegacyStepPath(absolute);
|
|
92
|
+
const canonCandidate = swapBvcExtension(absolute, { preferCanon });
|
|
93
|
+
if (canonCandidate !== absolute && existsSync(canonCandidate)) {
|
|
94
|
+
return canonCandidate;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const legacyCandidate = swapBvcExtension(absolute, { preferCanon: false });
|
|
98
|
+
if (legacyCandidate !== absolute && existsSync(legacyCandidate)) {
|
|
99
|
+
return legacyCandidate;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return absolute;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} filePath
|
|
107
|
+
* @param {{ cwd?: string, onLegacyStepRead?: (detail: { resolvedPath: string, message: string }) => void }} [options]
|
|
108
|
+
*/
|
|
109
|
+
export async function readBvcTextFile(filePath, options = {}) {
|
|
110
|
+
const cwd = options.cwd ?? process.cwd();
|
|
111
|
+
const resolved = resolveBvcReadablePath(filePath, cwd);
|
|
112
|
+
warnLegacyStepRead(resolved, options);
|
|
113
|
+
return readFile(resolved, 'utf8');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @param {string} filePath */
|
|
117
|
+
export function lintBvcFilePath(filePath) {
|
|
118
|
+
/** @type {Array<{ code: string, message: string }>} */
|
|
119
|
+
const lints = [];
|
|
120
|
+
if (isLegacyStepPath(filePath)) {
|
|
121
|
+
lints.push({
|
|
122
|
+
code: BVC_LINT_W_LEGACY_EXTENSION,
|
|
123
|
+
message: `Legacy extension ${BVC_EXTENSION_LEGACY}; prefer ${BVC_EXTENSION_CANON} for new writes (ADR adr-bvc-format-naming.md).`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return lints;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} content
|
|
131
|
+
* @param {{ filePath?: string, stripFilePragma?: boolean }} [options]
|
|
132
|
+
*/
|
|
133
|
+
export function parseBvcFileContent(content, options = {}) {
|
|
134
|
+
const document = parseBvcDocument(content);
|
|
135
|
+
const atoms = parseStepAtomDrafts(content, {
|
|
136
|
+
filePragmaLang: document.fileLang,
|
|
137
|
+
stripFilePragma: options.stripFilePragma !== false,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const pathLints = options.filePath ? lintBvcFilePath(options.filePath) : [];
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
schema: 'bvc-file.parse.v1',
|
|
144
|
+
filePath: options.filePath ?? null,
|
|
145
|
+
extension: options.filePath ? bvcExtensionFromPath(options.filePath) : null,
|
|
146
|
+
document,
|
|
147
|
+
atoms,
|
|
148
|
+
pathLints,
|
|
149
|
+
lints: [
|
|
150
|
+
...pathLints,
|
|
151
|
+
...atoms.flatMap((entry) => entry.lints ?? []),
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {ReturnType<typeof parseBvcFileContent>} left
|
|
158
|
+
* @param {ReturnType<typeof parseBvcFileContent>} right
|
|
159
|
+
*/
|
|
160
|
+
export function bvcParseResultsEquivalent(left, right) {
|
|
161
|
+
if (left.atoms.length !== right.atoms.length) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
for (let index = 0; index < left.atoms.length; index += 1) {
|
|
165
|
+
const a = left.atoms[index]?.ast;
|
|
166
|
+
const b = right.atoms[index]?.ast;
|
|
167
|
+
if (!a || !b) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (JSON.stringify(a.bvc) !== JSON.stringify(b.bvc)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (JSON.stringify(a.labels) !== JSON.stringify(b.labels)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
if (a.profile !== b.profile) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Canonical formatter: preserve resolved atom.lang and optional file pragma.
|
|
185
|
+
* @param {string} content
|
|
186
|
+
* @param {{ filePath?: string, stripFilePragma?: boolean }} [options]
|
|
187
|
+
*/
|
|
188
|
+
export function formatBvcFileContent(content, options = {}) {
|
|
189
|
+
const parsed = parseBvcFileContent(content, options);
|
|
190
|
+
const blocks = [];
|
|
191
|
+
|
|
192
|
+
if (parsed.document.fileLang) {
|
|
193
|
+
blocks.push(`#!bvc lang=${parsed.document.fileLang}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const atom of parsed.atoms) {
|
|
197
|
+
if (atom.errors?.length > 0) {
|
|
198
|
+
const error = new Error(
|
|
199
|
+
`Cannot format atom ${atom.draft.name}: ${atom.errors.join('; ')}`,
|
|
200
|
+
);
|
|
201
|
+
error.code = 'E_BVC_FORMAT_INVALID_ATOM';
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const draft = {
|
|
206
|
+
...atom.draft,
|
|
207
|
+
profile: atom.draft.profile || atom.draft.labels?.['atom.profile'] || '',
|
|
208
|
+
lang: atom.draft.lang,
|
|
209
|
+
};
|
|
210
|
+
blocks.push(formatStepAtomDraft(draft).trimEnd());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (blocks.length === 0) {
|
|
214
|
+
return parsed.document.fileLang ? `#!bvc lang=${parsed.document.fileLang}\n` : '';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return `${blocks.join('\n\n')}\n`;
|
|
218
|
+
}
|