@contractspec/module.workspace 4.0.0 → 4.1.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/dist/analysis/docblocks/diagnostics.d.ts +12 -0
- package/dist/analysis/docblocks/evaluator.d.ts +10 -0
- package/dist/analysis/docblocks/index.d.ts +4 -0
- package/dist/analysis/docblocks/list-source-files.d.ts +2 -0
- package/dist/analysis/docblocks/manifest.d.ts +14 -0
- package/dist/analysis/docblocks/static-values.d.ts +6 -0
- package/dist/analysis/docblocks.test.d.ts +1 -0
- package/dist/analysis/index.d.ts +1 -0
- package/dist/index.js +342 -4
- package/dist/node/index.js +342 -4
- package/package.json +7 -7
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DocBlockDiagnosticSeverity = 'error' | 'warning';
|
|
2
|
+
export interface DocBlockDiagnostic {
|
|
3
|
+
ruleId: string;
|
|
4
|
+
message: string;
|
|
5
|
+
severity: DocBlockDiagnosticSeverity;
|
|
6
|
+
file: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
column?: number;
|
|
9
|
+
context?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare function isAllowedDocOwnerModule(filePath: string, sourceModule: string): boolean;
|
|
12
|
+
export declare function formatDocBlockDiagnostics(diagnostics: readonly DocBlockDiagnostic[]): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DocBlockManifestEntry } from '@contractspec/lib.contracts-spec/docs';
|
|
2
|
+
export interface ModuleDocAnalysis {
|
|
3
|
+
entries: DocBlockManifestEntry[];
|
|
4
|
+
docRefs: string[];
|
|
5
|
+
docRefExports: Array<{
|
|
6
|
+
exportName: string;
|
|
7
|
+
refs: string[];
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export declare function extractModuleDocData(sourceText: string, filePath: string, sourceModule: string): ModuleDocAnalysis;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PackageDocManifest } from '@contractspec/lib.contracts-spec/docs';
|
|
2
|
+
import { type DocBlockDiagnostic } from './diagnostics';
|
|
3
|
+
export interface PackageDocAnalysisResult {
|
|
4
|
+
manifest: PackageDocManifest;
|
|
5
|
+
diagnostics: DocBlockDiagnostic[];
|
|
6
|
+
}
|
|
7
|
+
export declare function analyzePackageDocBlocks(options: {
|
|
8
|
+
packageName: string;
|
|
9
|
+
srcRoot: string;
|
|
10
|
+
}): PackageDocAnalysisResult;
|
|
11
|
+
export declare function buildPackageDocManifest(options: {
|
|
12
|
+
packageName: string;
|
|
13
|
+
srcRoot: string;
|
|
14
|
+
}): PackageDocManifest;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
export declare function evaluateExpression(expression: ts.Expression, locals: Map<string, JsonValue>, filePath: string): JsonValue | undefined;
|
|
6
|
+
export declare function collectLocalValues(sourceFile: ts.SourceFile, filePath: string): Map<string, JsonValue>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/analysis/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -918,6 +918,337 @@ function compareStructuralHints(diffs, a, b) {
|
|
|
918
918
|
label: "definition section presence"
|
|
919
919
|
});
|
|
920
920
|
}
|
|
921
|
+
// src/analysis/docblocks/diagnostics.ts
|
|
922
|
+
import path from "path";
|
|
923
|
+
var OWNER_SUFFIX_PATTERNS = [
|
|
924
|
+
".feature",
|
|
925
|
+
".command",
|
|
926
|
+
".query",
|
|
927
|
+
".operation",
|
|
928
|
+
".event",
|
|
929
|
+
".form",
|
|
930
|
+
".view",
|
|
931
|
+
".presentation",
|
|
932
|
+
".capability",
|
|
933
|
+
".dataView",
|
|
934
|
+
".data-view",
|
|
935
|
+
".service"
|
|
936
|
+
];
|
|
937
|
+
function isAllowedDocOwnerModule(filePath, sourceModule) {
|
|
938
|
+
const normalizedFilePath = filePath.replaceAll("\\", "/");
|
|
939
|
+
const normalizedSourceModule = sourceModule.replaceAll("\\", "/");
|
|
940
|
+
const baseName = path.posix.basename(normalizedSourceModule);
|
|
941
|
+
if (baseName === "index" || baseName === "spec" || baseName === "feature" || baseName === "extension") {
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
if (OWNER_SUFFIX_PATTERNS.some((suffix) => baseName.endsWith(suffix))) {
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
if (/\/(commands|queries|events|forms|views|presentations|capabilities|services)\//.test(normalizedFilePath)) {
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
return !/\/docs\//.test(normalizedFilePath);
|
|
951
|
+
}
|
|
952
|
+
function formatDocBlockDiagnostics(diagnostics) {
|
|
953
|
+
return diagnostics.map((diagnostic) => {
|
|
954
|
+
const location = diagnostic.line && diagnostic.column ? `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}` : diagnostic.file;
|
|
955
|
+
return `${diagnostic.ruleId}: ${diagnostic.message} (${location})`;
|
|
956
|
+
}).join(`
|
|
957
|
+
`);
|
|
958
|
+
}
|
|
959
|
+
// src/analysis/docblocks/evaluator.ts
|
|
960
|
+
import ts2 from "typescript";
|
|
961
|
+
|
|
962
|
+
// src/analysis/docblocks/static-values.ts
|
|
963
|
+
import ts from "typescript";
|
|
964
|
+
function evaluateExpression(expression, locals, filePath) {
|
|
965
|
+
if (ts.isParenthesizedExpression(expression)) {
|
|
966
|
+
return evaluateExpression(expression.expression, locals, filePath);
|
|
967
|
+
}
|
|
968
|
+
if (ts.isSatisfiesExpression(expression) || ts.isAsExpression(expression)) {
|
|
969
|
+
return evaluateExpression(expression.expression, locals, filePath);
|
|
970
|
+
}
|
|
971
|
+
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
972
|
+
return expression.text;
|
|
973
|
+
}
|
|
974
|
+
if (ts.isNumericLiteral(expression)) {
|
|
975
|
+
return Number(expression.text);
|
|
976
|
+
}
|
|
977
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword) {
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword) {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
if (expression.kind === ts.SyntaxKind.NullKeyword) {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
if (ts.isArrayLiteralExpression(expression)) {
|
|
987
|
+
const values = [];
|
|
988
|
+
for (const element of expression.elements) {
|
|
989
|
+
const value = evaluateExpression(element, locals, filePath);
|
|
990
|
+
if (value === undefined) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
values.push(value);
|
|
994
|
+
}
|
|
995
|
+
return values;
|
|
996
|
+
}
|
|
997
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
998
|
+
const value = {};
|
|
999
|
+
for (const property of expression.properties) {
|
|
1000
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const name = property.name.getText().replace(/^["']|["']$/g, "");
|
|
1004
|
+
const propertyValue = evaluateExpression(property.initializer, locals, filePath);
|
|
1005
|
+
if (propertyValue === undefined) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
value[name] = propertyValue;
|
|
1009
|
+
}
|
|
1010
|
+
return value;
|
|
1011
|
+
}
|
|
1012
|
+
if (ts.isIdentifier(expression)) {
|
|
1013
|
+
return locals.get(expression.text);
|
|
1014
|
+
}
|
|
1015
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1016
|
+
const parent = evaluateExpression(expression.expression, locals, filePath);
|
|
1017
|
+
if (!parent || typeof parent !== "object" || Array.isArray(parent)) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
return parent[expression.name.text];
|
|
1021
|
+
}
|
|
1022
|
+
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
1023
|
+
if ((expression.expression.text === "docId" || expression.expression.text === "docRef") && expression.arguments.length === 1) {
|
|
1024
|
+
return evaluateExpression(expression.arguments[0], locals, filePath);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
function collectLocalValues(sourceFile, filePath) {
|
|
1030
|
+
const locals = new Map;
|
|
1031
|
+
for (const statement of sourceFile.statements) {
|
|
1032
|
+
if (!ts.isVariableStatement(statement)) {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1036
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const value = evaluateExpression(declaration.initializer, locals, filePath);
|
|
1040
|
+
if (value !== undefined) {
|
|
1041
|
+
locals.set(declaration.name.text, value);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return locals;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/analysis/docblocks/evaluator.ts
|
|
1049
|
+
function isDocBlockValue(value) {
|
|
1050
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
const candidate = value;
|
|
1054
|
+
return typeof candidate.id === "string" && typeof candidate.title === "string" && typeof candidate.body === "string";
|
|
1055
|
+
}
|
|
1056
|
+
function extractDocRefsFromExpression(expression, locals, filePath) {
|
|
1057
|
+
let candidate = expression;
|
|
1058
|
+
while (ts2.isParenthesizedExpression(candidate) || ts2.isSatisfiesExpression(candidate) || ts2.isAsExpression(candidate)) {
|
|
1059
|
+
candidate = candidate.expression;
|
|
1060
|
+
}
|
|
1061
|
+
if (!ts2.isObjectLiteralExpression(candidate)) {
|
|
1062
|
+
return [];
|
|
1063
|
+
}
|
|
1064
|
+
const metaProperty = candidate.properties.find((property) => ts2.isPropertyAssignment(property) && property.name.getText() === "meta");
|
|
1065
|
+
if (!metaProperty || !ts2.isPropertyAssignment(metaProperty)) {
|
|
1066
|
+
return [];
|
|
1067
|
+
}
|
|
1068
|
+
let metaInitializer = metaProperty.initializer;
|
|
1069
|
+
while (ts2.isParenthesizedExpression(metaInitializer) || ts2.isSatisfiesExpression(metaInitializer) || ts2.isAsExpression(metaInitializer)) {
|
|
1070
|
+
metaInitializer = metaInitializer.expression;
|
|
1071
|
+
}
|
|
1072
|
+
if (!ts2.isObjectLiteralExpression(metaInitializer)) {
|
|
1073
|
+
return [];
|
|
1074
|
+
}
|
|
1075
|
+
const docIdProperty = metaInitializer.properties.find((property) => ts2.isPropertyAssignment(property) && property.name.getText() === "docId");
|
|
1076
|
+
if (!docIdProperty || !ts2.isPropertyAssignment(docIdProperty)) {
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
const refs = evaluateExpression(docIdProperty.initializer, locals, filePath);
|
|
1080
|
+
if (!Array.isArray(refs) || !refs.every((entry) => typeof entry === "string")) {
|
|
1081
|
+
throw new Error(`Non-static DocBlock reference in ${filePath} at ${docIdProperty.initializer.getStart()}`);
|
|
1082
|
+
}
|
|
1083
|
+
return refs;
|
|
1084
|
+
}
|
|
1085
|
+
function extractModuleDocData(sourceText, filePath, sourceModule) {
|
|
1086
|
+
const sourceFile = ts2.createSourceFile(filePath, sourceText, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
1087
|
+
const locals = collectLocalValues(sourceFile, filePath);
|
|
1088
|
+
const entries = [];
|
|
1089
|
+
const docRefs = new Set;
|
|
1090
|
+
const docRefExports = [];
|
|
1091
|
+
for (const statement of sourceFile.statements) {
|
|
1092
|
+
if (!ts2.isVariableStatement(statement)) {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
const isExported = statement.modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword);
|
|
1096
|
+
if (!isExported) {
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1100
|
+
if (!ts2.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const value = evaluateExpression(declaration.initializer, locals, filePath);
|
|
1104
|
+
const declarationRefs = extractDocRefsFromExpression(declaration.initializer, locals, filePath);
|
|
1105
|
+
for (const ref of declarationRefs) {
|
|
1106
|
+
docRefs.add(ref);
|
|
1107
|
+
}
|
|
1108
|
+
if (declarationRefs.length > 0) {
|
|
1109
|
+
docRefExports.push({
|
|
1110
|
+
exportName: declaration.name.text,
|
|
1111
|
+
refs: declarationRefs
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
if (value === undefined) {
|
|
1115
|
+
if (declaration.name.text.includes("DocBlock") || declaration.initializer.getText(sourceFile).includes("satisfies DocBlock")) {
|
|
1116
|
+
throw new Error(`Non-static DocBlock source in ${filePath} at ${declaration.initializer.getStart(sourceFile)}`);
|
|
1117
|
+
}
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (isDocBlockValue(value)) {
|
|
1121
|
+
entries.push({
|
|
1122
|
+
id: value.id,
|
|
1123
|
+
exportName: declaration.name.text,
|
|
1124
|
+
sourceModule,
|
|
1125
|
+
block: value
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
if (Array.isArray(value) && value.every((entry) => isDocBlockValue(entry))) {
|
|
1129
|
+
for (const block of value) {
|
|
1130
|
+
entries.push({
|
|
1131
|
+
id: block.id,
|
|
1132
|
+
exportName: declaration.name.text,
|
|
1133
|
+
sourceModule,
|
|
1134
|
+
block
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return { entries, docRefs: [...docRefs], docRefExports };
|
|
1141
|
+
}
|
|
1142
|
+
// src/analysis/docblocks/list-source-files.ts
|
|
1143
|
+
import fs from "fs";
|
|
1144
|
+
import path2 from "path";
|
|
1145
|
+
function shouldIgnoreSourceFile(filePath) {
|
|
1146
|
+
return !/\.[cm]?[jt]sx?$/.test(filePath) || filePath.endsWith(".d.ts") || filePath.endsWith(".test.ts") || filePath.endsWith(".generated.ts") || filePath.includes(`${path2.sep}dist${path2.sep}`);
|
|
1147
|
+
}
|
|
1148
|
+
function listSourceFiles(srcRoot) {
|
|
1149
|
+
return fs.readdirSync(srcRoot, { withFileTypes: true }).flatMap((entry) => {
|
|
1150
|
+
const absolutePath = path2.join(srcRoot, entry.name);
|
|
1151
|
+
if (entry.isDirectory()) {
|
|
1152
|
+
return listSourceFiles(absolutePath);
|
|
1153
|
+
}
|
|
1154
|
+
return shouldIgnoreSourceFile(absolutePath) ? [] : [absolutePath];
|
|
1155
|
+
}).sort((left, right) => left.localeCompare(right));
|
|
1156
|
+
}
|
|
1157
|
+
function toSourceModule(srcRoot, filePath) {
|
|
1158
|
+
return path2.relative(srcRoot, filePath).replace(/\\/g, "/").replace(/\.[cm]?[jt]sx?$/, "");
|
|
1159
|
+
}
|
|
1160
|
+
// src/analysis/docblocks/manifest.ts
|
|
1161
|
+
import fs2 from "fs";
|
|
1162
|
+
function createDiagnostic(ruleId, message, file, context) {
|
|
1163
|
+
return {
|
|
1164
|
+
ruleId,
|
|
1165
|
+
message,
|
|
1166
|
+
severity: "error",
|
|
1167
|
+
file,
|
|
1168
|
+
context
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function analyzePackageDocBlocks(options) {
|
|
1172
|
+
const { packageName, srcRoot } = options;
|
|
1173
|
+
const diagnostics = [];
|
|
1174
|
+
const entries = [];
|
|
1175
|
+
const seenIds = new Map;
|
|
1176
|
+
const seenRoutes = new Map;
|
|
1177
|
+
const moduleRecords = new Map;
|
|
1178
|
+
for (const filePath of listSourceFiles(srcRoot)) {
|
|
1179
|
+
if (filePath.endsWith(".docblock.ts")) {
|
|
1180
|
+
diagnostics.push(createDiagnostic("docblock-standalone-source", "Standalone DocBlock sources are not allowed.", filePath));
|
|
1181
|
+
}
|
|
1182
|
+
if (filePath.includes("/docs/tech/") || filePath.includes("\\docs\\tech\\")) {
|
|
1183
|
+
diagnostics.push(createDiagnostic("docblock-tech-folder", "docs/tech source files are not allowed.", filePath));
|
|
1184
|
+
}
|
|
1185
|
+
const sourceModule = toSourceModule(srcRoot, filePath);
|
|
1186
|
+
try {
|
|
1187
|
+
const moduleData = extractModuleDocData(fs2.readFileSync(filePath, "utf8"), filePath, sourceModule);
|
|
1188
|
+
moduleRecords.set(sourceModule, {
|
|
1189
|
+
entries: moduleData.entries,
|
|
1190
|
+
docRefExports: moduleData.docRefExports,
|
|
1191
|
+
filePath,
|
|
1192
|
+
sourceModule
|
|
1193
|
+
});
|
|
1194
|
+
if (moduleData.entries.length > 0 && !isAllowedDocOwnerModule(filePath, sourceModule)) {
|
|
1195
|
+
diagnostics.push(createDiagnostic("docblock-orphan-owner", "DocBlocks must be exported from the owner module, not a docs-only helper file.", filePath, { sourceModule }));
|
|
1196
|
+
}
|
|
1197
|
+
for (const entry of moduleData.entries) {
|
|
1198
|
+
const sourceRef = `${entry.sourceModule}:${entry.exportName}`;
|
|
1199
|
+
const priorId = seenIds.get(entry.id);
|
|
1200
|
+
if (priorId) {
|
|
1201
|
+
diagnostics.push(createDiagnostic("docblock-duplicate-id", `Duplicate DocBlock id ${entry.id} in ${sourceRef} and ${priorId}.`, filePath, { docId: entry.id, prior: priorId, sourceRef }));
|
|
1202
|
+
} else {
|
|
1203
|
+
seenIds.set(entry.id, sourceRef);
|
|
1204
|
+
}
|
|
1205
|
+
if (entry.block.route) {
|
|
1206
|
+
const priorRoute = seenRoutes.get(entry.block.route);
|
|
1207
|
+
if (priorRoute) {
|
|
1208
|
+
diagnostics.push(createDiagnostic("docblock-duplicate-route", `Duplicate DocBlock route ${entry.block.route} in ${sourceRef} and ${priorRoute}.`, filePath, { route: entry.block.route, prior: priorRoute, sourceRef }));
|
|
1209
|
+
} else {
|
|
1210
|
+
seenRoutes.set(entry.block.route, sourceRef);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
entries.push(entry);
|
|
1214
|
+
}
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
diagnostics.push(createDiagnostic("docblock-non-static-source", error instanceof Error ? error.message : `Non-static DocBlock source in ${filePath}.`, filePath));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for (const record of moduleRecords.values()) {
|
|
1220
|
+
const localIds = new Set(record.entries.map((entry) => entry.id));
|
|
1221
|
+
for (const docRefExport of record.docRefExports) {
|
|
1222
|
+
for (const ref of docRefExport.refs) {
|
|
1223
|
+
if (localIds.has(ref)) {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (seenIds.has(ref)) {
|
|
1227
|
+
diagnostics.push(createDiagnostic("docblock-cross-file-reference", `${docRefExport.exportName} references DocBlock ${ref}, but the DocBlock is not exported from the same module.`, record.filePath, { docId: ref, exportName: docRefExport.exportName }));
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
diagnostics.push(createDiagnostic("docblock-missing-ref", `${docRefExport.exportName} references missing DocBlock ${ref}.`, record.filePath, { docId: ref, exportName: docRefExport.exportName }));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
entries.sort((left, right) => left.id.localeCompare(right.id));
|
|
1235
|
+
return {
|
|
1236
|
+
manifest: {
|
|
1237
|
+
packageName,
|
|
1238
|
+
generatedAt: new Date().toISOString(),
|
|
1239
|
+
blocks: entries
|
|
1240
|
+
},
|
|
1241
|
+
diagnostics
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function buildPackageDocManifest(options) {
|
|
1245
|
+
const analysis = analyzePackageDocBlocks(options);
|
|
1246
|
+
const failures = analysis.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1247
|
+
if (failures.length > 0) {
|
|
1248
|
+
throw new Error(formatDocBlockDiagnostics(failures));
|
|
1249
|
+
}
|
|
1250
|
+
return analysis.manifest;
|
|
1251
|
+
}
|
|
921
1252
|
// src/analysis/example-scan.ts
|
|
922
1253
|
function isExampleFile(filePath) {
|
|
923
1254
|
return filePath.includes("/example.") || filePath.endsWith(".example.ts");
|
|
@@ -1694,8 +2025,8 @@ function sortFields(fields) {
|
|
|
1694
2025
|
// src/analysis/snapshot/snapshot.ts
|
|
1695
2026
|
function generateSnapshot(specs, options = {}) {
|
|
1696
2027
|
const snapshots = [];
|
|
1697
|
-
for (const { path, content } of specs) {
|
|
1698
|
-
const scanned = scanSpecSource(content,
|
|
2028
|
+
for (const { path: path3, content } of specs) {
|
|
2029
|
+
const scanned = scanSpecSource(content, path3);
|
|
1699
2030
|
if (options.types && !options.types.includes(scanned.specType)) {
|
|
1700
2031
|
continue;
|
|
1701
2032
|
}
|
|
@@ -2657,7 +2988,7 @@ async function formatFilesBatch(files, config, options, logger) {
|
|
|
2657
2988
|
return formatFiles(files, config, options, logger);
|
|
2658
2989
|
}
|
|
2659
2990
|
// src/formatters/spec-markdown.ts
|
|
2660
|
-
import * as
|
|
2991
|
+
import * as path3 from "path";
|
|
2661
2992
|
function specToMarkdown(spec, format, optionsOrDepth = 0) {
|
|
2662
2993
|
const options = typeof optionsOrDepth === "number" ? { depth: optionsOrDepth } : optionsOrDepth;
|
|
2663
2994
|
const depth = options.depth ?? 0;
|
|
@@ -2782,7 +3113,7 @@ function formatFullMode(spec, lines, indent, rootPath) {
|
|
|
2782
3113
|
lines.push(`${indent}- **Tags**: ${spec.meta.tags.join(", ")}`);
|
|
2783
3114
|
}
|
|
2784
3115
|
if (spec.filePath) {
|
|
2785
|
-
const displayPath = rootPath ?
|
|
3116
|
+
const displayPath = rootPath ? path3.relative(rootPath, spec.filePath) : spec.filePath;
|
|
2786
3117
|
lines.push(`${indent}- **File**: \`${displayPath}\``);
|
|
2787
3118
|
}
|
|
2788
3119
|
lines.push("");
|
|
@@ -4191,6 +4522,7 @@ export const ${runnerName} = new WorkflowRunner({
|
|
|
4191
4522
|
}
|
|
4192
4523
|
export {
|
|
4193
4524
|
validateSpecStructure,
|
|
4525
|
+
toSourceModule,
|
|
4194
4526
|
toPascalCase,
|
|
4195
4527
|
toKebabCase,
|
|
4196
4528
|
toDot,
|
|
@@ -4207,9 +4539,11 @@ export {
|
|
|
4207
4539
|
parseImportedSpecNames,
|
|
4208
4540
|
normalizeValue,
|
|
4209
4541
|
loadSpecFromSource,
|
|
4542
|
+
listSourceFiles,
|
|
4210
4543
|
isFeatureFile,
|
|
4211
4544
|
isExampleFile,
|
|
4212
4545
|
isBreakingChange,
|
|
4546
|
+
isAllowedDocOwnerModule,
|
|
4213
4547
|
inferSpecTypeFromFilePath,
|
|
4214
4548
|
inferSpecTypeFromCodeBlock,
|
|
4215
4549
|
groupSpecsToArray,
|
|
@@ -4239,12 +4573,14 @@ export {
|
|
|
4239
4573
|
generateAppBlueprintSpec,
|
|
4240
4574
|
formatFilesBatch,
|
|
4241
4575
|
formatFiles,
|
|
4576
|
+
formatDocBlockDiagnostics,
|
|
4242
4577
|
findMissingDependencies,
|
|
4243
4578
|
findMatchingRule,
|
|
4244
4579
|
filterSpecs,
|
|
4245
4580
|
filterFeatures,
|
|
4246
4581
|
extractTestTarget,
|
|
4247
4582
|
extractTestCoverage,
|
|
4583
|
+
extractModuleDocData,
|
|
4248
4584
|
escapeString,
|
|
4249
4585
|
detectFormatter,
|
|
4250
4586
|
detectCycles,
|
|
@@ -4261,11 +4597,13 @@ export {
|
|
|
4261
4597
|
buildTestPrompt,
|
|
4262
4598
|
buildReverseEdges,
|
|
4263
4599
|
buildPresentationSpecPrompt,
|
|
4600
|
+
buildPackageDocManifest,
|
|
4264
4601
|
buildOperationSpecPrompt,
|
|
4265
4602
|
buildHandlerPrompt,
|
|
4266
4603
|
buildFormPrompt,
|
|
4267
4604
|
buildEventSpecPrompt,
|
|
4268
4605
|
buildComponentPrompt,
|
|
4606
|
+
analyzePackageDocBlocks,
|
|
4269
4607
|
addExampleContext,
|
|
4270
4608
|
addContractNode,
|
|
4271
4609
|
SpecGroupingStrategies,
|
package/dist/node/index.js
CHANGED
|
@@ -917,6 +917,337 @@ function compareStructuralHints(diffs, a, b) {
|
|
|
917
917
|
label: "definition section presence"
|
|
918
918
|
});
|
|
919
919
|
}
|
|
920
|
+
// src/analysis/docblocks/diagnostics.ts
|
|
921
|
+
import path from "node:path";
|
|
922
|
+
var OWNER_SUFFIX_PATTERNS = [
|
|
923
|
+
".feature",
|
|
924
|
+
".command",
|
|
925
|
+
".query",
|
|
926
|
+
".operation",
|
|
927
|
+
".event",
|
|
928
|
+
".form",
|
|
929
|
+
".view",
|
|
930
|
+
".presentation",
|
|
931
|
+
".capability",
|
|
932
|
+
".dataView",
|
|
933
|
+
".data-view",
|
|
934
|
+
".service"
|
|
935
|
+
];
|
|
936
|
+
function isAllowedDocOwnerModule(filePath, sourceModule) {
|
|
937
|
+
const normalizedFilePath = filePath.replaceAll("\\", "/");
|
|
938
|
+
const normalizedSourceModule = sourceModule.replaceAll("\\", "/");
|
|
939
|
+
const baseName = path.posix.basename(normalizedSourceModule);
|
|
940
|
+
if (baseName === "index" || baseName === "spec" || baseName === "feature" || baseName === "extension") {
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
if (OWNER_SUFFIX_PATTERNS.some((suffix) => baseName.endsWith(suffix))) {
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
if (/\/(commands|queries|events|forms|views|presentations|capabilities|services)\//.test(normalizedFilePath)) {
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
return !/\/docs\//.test(normalizedFilePath);
|
|
950
|
+
}
|
|
951
|
+
function formatDocBlockDiagnostics(diagnostics) {
|
|
952
|
+
return diagnostics.map((diagnostic) => {
|
|
953
|
+
const location = diagnostic.line && diagnostic.column ? `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}` : diagnostic.file;
|
|
954
|
+
return `${diagnostic.ruleId}: ${diagnostic.message} (${location})`;
|
|
955
|
+
}).join(`
|
|
956
|
+
`);
|
|
957
|
+
}
|
|
958
|
+
// src/analysis/docblocks/evaluator.ts
|
|
959
|
+
import ts2 from "typescript";
|
|
960
|
+
|
|
961
|
+
// src/analysis/docblocks/static-values.ts
|
|
962
|
+
import ts from "typescript";
|
|
963
|
+
function evaluateExpression(expression, locals, filePath) {
|
|
964
|
+
if (ts.isParenthesizedExpression(expression)) {
|
|
965
|
+
return evaluateExpression(expression.expression, locals, filePath);
|
|
966
|
+
}
|
|
967
|
+
if (ts.isSatisfiesExpression(expression) || ts.isAsExpression(expression)) {
|
|
968
|
+
return evaluateExpression(expression.expression, locals, filePath);
|
|
969
|
+
}
|
|
970
|
+
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
971
|
+
return expression.text;
|
|
972
|
+
}
|
|
973
|
+
if (ts.isNumericLiteral(expression)) {
|
|
974
|
+
return Number(expression.text);
|
|
975
|
+
}
|
|
976
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword) {
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
if (expression.kind === ts.SyntaxKind.NullKeyword) {
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
if (ts.isArrayLiteralExpression(expression)) {
|
|
986
|
+
const values = [];
|
|
987
|
+
for (const element of expression.elements) {
|
|
988
|
+
const value = evaluateExpression(element, locals, filePath);
|
|
989
|
+
if (value === undefined) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
values.push(value);
|
|
993
|
+
}
|
|
994
|
+
return values;
|
|
995
|
+
}
|
|
996
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
997
|
+
const value = {};
|
|
998
|
+
for (const property of expression.properties) {
|
|
999
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const name = property.name.getText().replace(/^["']|["']$/g, "");
|
|
1003
|
+
const propertyValue = evaluateExpression(property.initializer, locals, filePath);
|
|
1004
|
+
if (propertyValue === undefined) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
value[name] = propertyValue;
|
|
1008
|
+
}
|
|
1009
|
+
return value;
|
|
1010
|
+
}
|
|
1011
|
+
if (ts.isIdentifier(expression)) {
|
|
1012
|
+
return locals.get(expression.text);
|
|
1013
|
+
}
|
|
1014
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1015
|
+
const parent = evaluateExpression(expression.expression, locals, filePath);
|
|
1016
|
+
if (!parent || typeof parent !== "object" || Array.isArray(parent)) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
return parent[expression.name.text];
|
|
1020
|
+
}
|
|
1021
|
+
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
1022
|
+
if ((expression.expression.text === "docId" || expression.expression.text === "docRef") && expression.arguments.length === 1) {
|
|
1023
|
+
return evaluateExpression(expression.arguments[0], locals, filePath);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
function collectLocalValues(sourceFile, filePath) {
|
|
1029
|
+
const locals = new Map;
|
|
1030
|
+
for (const statement of sourceFile.statements) {
|
|
1031
|
+
if (!ts.isVariableStatement(statement)) {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1035
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const value = evaluateExpression(declaration.initializer, locals, filePath);
|
|
1039
|
+
if (value !== undefined) {
|
|
1040
|
+
locals.set(declaration.name.text, value);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return locals;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/analysis/docblocks/evaluator.ts
|
|
1048
|
+
function isDocBlockValue(value) {
|
|
1049
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
const candidate = value;
|
|
1053
|
+
return typeof candidate.id === "string" && typeof candidate.title === "string" && typeof candidate.body === "string";
|
|
1054
|
+
}
|
|
1055
|
+
function extractDocRefsFromExpression(expression, locals, filePath) {
|
|
1056
|
+
let candidate = expression;
|
|
1057
|
+
while (ts2.isParenthesizedExpression(candidate) || ts2.isSatisfiesExpression(candidate) || ts2.isAsExpression(candidate)) {
|
|
1058
|
+
candidate = candidate.expression;
|
|
1059
|
+
}
|
|
1060
|
+
if (!ts2.isObjectLiteralExpression(candidate)) {
|
|
1061
|
+
return [];
|
|
1062
|
+
}
|
|
1063
|
+
const metaProperty = candidate.properties.find((property) => ts2.isPropertyAssignment(property) && property.name.getText() === "meta");
|
|
1064
|
+
if (!metaProperty || !ts2.isPropertyAssignment(metaProperty)) {
|
|
1065
|
+
return [];
|
|
1066
|
+
}
|
|
1067
|
+
let metaInitializer = metaProperty.initializer;
|
|
1068
|
+
while (ts2.isParenthesizedExpression(metaInitializer) || ts2.isSatisfiesExpression(metaInitializer) || ts2.isAsExpression(metaInitializer)) {
|
|
1069
|
+
metaInitializer = metaInitializer.expression;
|
|
1070
|
+
}
|
|
1071
|
+
if (!ts2.isObjectLiteralExpression(metaInitializer)) {
|
|
1072
|
+
return [];
|
|
1073
|
+
}
|
|
1074
|
+
const docIdProperty = metaInitializer.properties.find((property) => ts2.isPropertyAssignment(property) && property.name.getText() === "docId");
|
|
1075
|
+
if (!docIdProperty || !ts2.isPropertyAssignment(docIdProperty)) {
|
|
1076
|
+
return [];
|
|
1077
|
+
}
|
|
1078
|
+
const refs = evaluateExpression(docIdProperty.initializer, locals, filePath);
|
|
1079
|
+
if (!Array.isArray(refs) || !refs.every((entry) => typeof entry === "string")) {
|
|
1080
|
+
throw new Error(`Non-static DocBlock reference in ${filePath} at ${docIdProperty.initializer.getStart()}`);
|
|
1081
|
+
}
|
|
1082
|
+
return refs;
|
|
1083
|
+
}
|
|
1084
|
+
function extractModuleDocData(sourceText, filePath, sourceModule) {
|
|
1085
|
+
const sourceFile = ts2.createSourceFile(filePath, sourceText, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
1086
|
+
const locals = collectLocalValues(sourceFile, filePath);
|
|
1087
|
+
const entries = [];
|
|
1088
|
+
const docRefs = new Set;
|
|
1089
|
+
const docRefExports = [];
|
|
1090
|
+
for (const statement of sourceFile.statements) {
|
|
1091
|
+
if (!ts2.isVariableStatement(statement)) {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const isExported = statement.modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword);
|
|
1095
|
+
if (!isExported) {
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1099
|
+
if (!ts2.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const value = evaluateExpression(declaration.initializer, locals, filePath);
|
|
1103
|
+
const declarationRefs = extractDocRefsFromExpression(declaration.initializer, locals, filePath);
|
|
1104
|
+
for (const ref of declarationRefs) {
|
|
1105
|
+
docRefs.add(ref);
|
|
1106
|
+
}
|
|
1107
|
+
if (declarationRefs.length > 0) {
|
|
1108
|
+
docRefExports.push({
|
|
1109
|
+
exportName: declaration.name.text,
|
|
1110
|
+
refs: declarationRefs
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
if (value === undefined) {
|
|
1114
|
+
if (declaration.name.text.includes("DocBlock") || declaration.initializer.getText(sourceFile).includes("satisfies DocBlock")) {
|
|
1115
|
+
throw new Error(`Non-static DocBlock source in ${filePath} at ${declaration.initializer.getStart(sourceFile)}`);
|
|
1116
|
+
}
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if (isDocBlockValue(value)) {
|
|
1120
|
+
entries.push({
|
|
1121
|
+
id: value.id,
|
|
1122
|
+
exportName: declaration.name.text,
|
|
1123
|
+
sourceModule,
|
|
1124
|
+
block: value
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
if (Array.isArray(value) && value.every((entry) => isDocBlockValue(entry))) {
|
|
1128
|
+
for (const block of value) {
|
|
1129
|
+
entries.push({
|
|
1130
|
+
id: block.id,
|
|
1131
|
+
exportName: declaration.name.text,
|
|
1132
|
+
sourceModule,
|
|
1133
|
+
block
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return { entries, docRefs: [...docRefs], docRefExports };
|
|
1140
|
+
}
|
|
1141
|
+
// src/analysis/docblocks/list-source-files.ts
|
|
1142
|
+
import fs from "node:fs";
|
|
1143
|
+
import path2 from "node:path";
|
|
1144
|
+
function shouldIgnoreSourceFile(filePath) {
|
|
1145
|
+
return !/\.[cm]?[jt]sx?$/.test(filePath) || filePath.endsWith(".d.ts") || filePath.endsWith(".test.ts") || filePath.endsWith(".generated.ts") || filePath.includes(`${path2.sep}dist${path2.sep}`);
|
|
1146
|
+
}
|
|
1147
|
+
function listSourceFiles(srcRoot) {
|
|
1148
|
+
return fs.readdirSync(srcRoot, { withFileTypes: true }).flatMap((entry) => {
|
|
1149
|
+
const absolutePath = path2.join(srcRoot, entry.name);
|
|
1150
|
+
if (entry.isDirectory()) {
|
|
1151
|
+
return listSourceFiles(absolutePath);
|
|
1152
|
+
}
|
|
1153
|
+
return shouldIgnoreSourceFile(absolutePath) ? [] : [absolutePath];
|
|
1154
|
+
}).sort((left, right) => left.localeCompare(right));
|
|
1155
|
+
}
|
|
1156
|
+
function toSourceModule(srcRoot, filePath) {
|
|
1157
|
+
return path2.relative(srcRoot, filePath).replace(/\\/g, "/").replace(/\.[cm]?[jt]sx?$/, "");
|
|
1158
|
+
}
|
|
1159
|
+
// src/analysis/docblocks/manifest.ts
|
|
1160
|
+
import fs2 from "node:fs";
|
|
1161
|
+
function createDiagnostic(ruleId, message, file, context) {
|
|
1162
|
+
return {
|
|
1163
|
+
ruleId,
|
|
1164
|
+
message,
|
|
1165
|
+
severity: "error",
|
|
1166
|
+
file,
|
|
1167
|
+
context
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function analyzePackageDocBlocks(options) {
|
|
1171
|
+
const { packageName, srcRoot } = options;
|
|
1172
|
+
const diagnostics = [];
|
|
1173
|
+
const entries = [];
|
|
1174
|
+
const seenIds = new Map;
|
|
1175
|
+
const seenRoutes = new Map;
|
|
1176
|
+
const moduleRecords = new Map;
|
|
1177
|
+
for (const filePath of listSourceFiles(srcRoot)) {
|
|
1178
|
+
if (filePath.endsWith(".docblock.ts")) {
|
|
1179
|
+
diagnostics.push(createDiagnostic("docblock-standalone-source", "Standalone DocBlock sources are not allowed.", filePath));
|
|
1180
|
+
}
|
|
1181
|
+
if (filePath.includes("/docs/tech/") || filePath.includes("\\docs\\tech\\")) {
|
|
1182
|
+
diagnostics.push(createDiagnostic("docblock-tech-folder", "docs/tech source files are not allowed.", filePath));
|
|
1183
|
+
}
|
|
1184
|
+
const sourceModule = toSourceModule(srcRoot, filePath);
|
|
1185
|
+
try {
|
|
1186
|
+
const moduleData = extractModuleDocData(fs2.readFileSync(filePath, "utf8"), filePath, sourceModule);
|
|
1187
|
+
moduleRecords.set(sourceModule, {
|
|
1188
|
+
entries: moduleData.entries,
|
|
1189
|
+
docRefExports: moduleData.docRefExports,
|
|
1190
|
+
filePath,
|
|
1191
|
+
sourceModule
|
|
1192
|
+
});
|
|
1193
|
+
if (moduleData.entries.length > 0 && !isAllowedDocOwnerModule(filePath, sourceModule)) {
|
|
1194
|
+
diagnostics.push(createDiagnostic("docblock-orphan-owner", "DocBlocks must be exported from the owner module, not a docs-only helper file.", filePath, { sourceModule }));
|
|
1195
|
+
}
|
|
1196
|
+
for (const entry of moduleData.entries) {
|
|
1197
|
+
const sourceRef = `${entry.sourceModule}:${entry.exportName}`;
|
|
1198
|
+
const priorId = seenIds.get(entry.id);
|
|
1199
|
+
if (priorId) {
|
|
1200
|
+
diagnostics.push(createDiagnostic("docblock-duplicate-id", `Duplicate DocBlock id ${entry.id} in ${sourceRef} and ${priorId}.`, filePath, { docId: entry.id, prior: priorId, sourceRef }));
|
|
1201
|
+
} else {
|
|
1202
|
+
seenIds.set(entry.id, sourceRef);
|
|
1203
|
+
}
|
|
1204
|
+
if (entry.block.route) {
|
|
1205
|
+
const priorRoute = seenRoutes.get(entry.block.route);
|
|
1206
|
+
if (priorRoute) {
|
|
1207
|
+
diagnostics.push(createDiagnostic("docblock-duplicate-route", `Duplicate DocBlock route ${entry.block.route} in ${sourceRef} and ${priorRoute}.`, filePath, { route: entry.block.route, prior: priorRoute, sourceRef }));
|
|
1208
|
+
} else {
|
|
1209
|
+
seenRoutes.set(entry.block.route, sourceRef);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
entries.push(entry);
|
|
1213
|
+
}
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
diagnostics.push(createDiagnostic("docblock-non-static-source", error instanceof Error ? error.message : `Non-static DocBlock source in ${filePath}.`, filePath));
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
for (const record of moduleRecords.values()) {
|
|
1219
|
+
const localIds = new Set(record.entries.map((entry) => entry.id));
|
|
1220
|
+
for (const docRefExport of record.docRefExports) {
|
|
1221
|
+
for (const ref of docRefExport.refs) {
|
|
1222
|
+
if (localIds.has(ref)) {
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (seenIds.has(ref)) {
|
|
1226
|
+
diagnostics.push(createDiagnostic("docblock-cross-file-reference", `${docRefExport.exportName} references DocBlock ${ref}, but the DocBlock is not exported from the same module.`, record.filePath, { docId: ref, exportName: docRefExport.exportName }));
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
diagnostics.push(createDiagnostic("docblock-missing-ref", `${docRefExport.exportName} references missing DocBlock ${ref}.`, record.filePath, { docId: ref, exportName: docRefExport.exportName }));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
entries.sort((left, right) => left.id.localeCompare(right.id));
|
|
1234
|
+
return {
|
|
1235
|
+
manifest: {
|
|
1236
|
+
packageName,
|
|
1237
|
+
generatedAt: new Date().toISOString(),
|
|
1238
|
+
blocks: entries
|
|
1239
|
+
},
|
|
1240
|
+
diagnostics
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function buildPackageDocManifest(options) {
|
|
1244
|
+
const analysis = analyzePackageDocBlocks(options);
|
|
1245
|
+
const failures = analysis.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1246
|
+
if (failures.length > 0) {
|
|
1247
|
+
throw new Error(formatDocBlockDiagnostics(failures));
|
|
1248
|
+
}
|
|
1249
|
+
return analysis.manifest;
|
|
1250
|
+
}
|
|
920
1251
|
// src/analysis/example-scan.ts
|
|
921
1252
|
function isExampleFile(filePath) {
|
|
922
1253
|
return filePath.includes("/example.") || filePath.endsWith(".example.ts");
|
|
@@ -1693,8 +2024,8 @@ function sortFields(fields) {
|
|
|
1693
2024
|
// src/analysis/snapshot/snapshot.ts
|
|
1694
2025
|
function generateSnapshot(specs, options = {}) {
|
|
1695
2026
|
const snapshots = [];
|
|
1696
|
-
for (const { path, content } of specs) {
|
|
1697
|
-
const scanned = scanSpecSource(content,
|
|
2027
|
+
for (const { path: path3, content } of specs) {
|
|
2028
|
+
const scanned = scanSpecSource(content, path3);
|
|
1698
2029
|
if (options.types && !options.types.includes(scanned.specType)) {
|
|
1699
2030
|
continue;
|
|
1700
2031
|
}
|
|
@@ -2656,7 +2987,7 @@ async function formatFilesBatch(files, config, options, logger) {
|
|
|
2656
2987
|
return formatFiles(files, config, options, logger);
|
|
2657
2988
|
}
|
|
2658
2989
|
// src/formatters/spec-markdown.ts
|
|
2659
|
-
import * as
|
|
2990
|
+
import * as path3 from "node:path";
|
|
2660
2991
|
function specToMarkdown(spec, format, optionsOrDepth = 0) {
|
|
2661
2992
|
const options = typeof optionsOrDepth === "number" ? { depth: optionsOrDepth } : optionsOrDepth;
|
|
2662
2993
|
const depth = options.depth ?? 0;
|
|
@@ -2781,7 +3112,7 @@ function formatFullMode(spec, lines, indent, rootPath) {
|
|
|
2781
3112
|
lines.push(`${indent}- **Tags**: ${spec.meta.tags.join(", ")}`);
|
|
2782
3113
|
}
|
|
2783
3114
|
if (spec.filePath) {
|
|
2784
|
-
const displayPath = rootPath ?
|
|
3115
|
+
const displayPath = rootPath ? path3.relative(rootPath, spec.filePath) : spec.filePath;
|
|
2785
3116
|
lines.push(`${indent}- **File**: \`${displayPath}\``);
|
|
2786
3117
|
}
|
|
2787
3118
|
lines.push("");
|
|
@@ -4190,6 +4521,7 @@ export const ${runnerName} = new WorkflowRunner({
|
|
|
4190
4521
|
}
|
|
4191
4522
|
export {
|
|
4192
4523
|
validateSpecStructure,
|
|
4524
|
+
toSourceModule,
|
|
4193
4525
|
toPascalCase,
|
|
4194
4526
|
toKebabCase,
|
|
4195
4527
|
toDot,
|
|
@@ -4206,9 +4538,11 @@ export {
|
|
|
4206
4538
|
parseImportedSpecNames,
|
|
4207
4539
|
normalizeValue,
|
|
4208
4540
|
loadSpecFromSource,
|
|
4541
|
+
listSourceFiles,
|
|
4209
4542
|
isFeatureFile,
|
|
4210
4543
|
isExampleFile,
|
|
4211
4544
|
isBreakingChange,
|
|
4545
|
+
isAllowedDocOwnerModule,
|
|
4212
4546
|
inferSpecTypeFromFilePath,
|
|
4213
4547
|
inferSpecTypeFromCodeBlock,
|
|
4214
4548
|
groupSpecsToArray,
|
|
@@ -4238,12 +4572,14 @@ export {
|
|
|
4238
4572
|
generateAppBlueprintSpec,
|
|
4239
4573
|
formatFilesBatch,
|
|
4240
4574
|
formatFiles,
|
|
4575
|
+
formatDocBlockDiagnostics,
|
|
4241
4576
|
findMissingDependencies,
|
|
4242
4577
|
findMatchingRule,
|
|
4243
4578
|
filterSpecs,
|
|
4244
4579
|
filterFeatures,
|
|
4245
4580
|
extractTestTarget,
|
|
4246
4581
|
extractTestCoverage,
|
|
4582
|
+
extractModuleDocData,
|
|
4247
4583
|
escapeString,
|
|
4248
4584
|
detectFormatter,
|
|
4249
4585
|
detectCycles,
|
|
@@ -4260,11 +4596,13 @@ export {
|
|
|
4260
4596
|
buildTestPrompt,
|
|
4261
4597
|
buildReverseEdges,
|
|
4262
4598
|
buildPresentationSpecPrompt,
|
|
4599
|
+
buildPackageDocManifest,
|
|
4263
4600
|
buildOperationSpecPrompt,
|
|
4264
4601
|
buildHandlerPrompt,
|
|
4265
4602
|
buildFormPrompt,
|
|
4266
4603
|
buildEventSpecPrompt,
|
|
4267
4604
|
buildComponentPrompt,
|
|
4605
|
+
analyzePackageDocBlocks,
|
|
4268
4606
|
addExampleContext,
|
|
4269
4607
|
addContractNode,
|
|
4270
4608
|
SpecGroupingStrategies,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/module.workspace",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Workspace discovery and management module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -31,17 +31,17 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@contractspec/lib.contracts-spec": "
|
|
35
|
-
"@contractspec/lib.schema": "3.7.
|
|
34
|
+
"@contractspec/lib.contracts-spec": "5.0.0",
|
|
35
|
+
"@contractspec/lib.schema": "3.7.10",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
36
37
|
"compare-versions": "^6.1.1",
|
|
37
38
|
"ts-morph": "^27.0.2",
|
|
38
39
|
"zod": "^4.3.5",
|
|
39
|
-
"@contractspec/lib.contracts-integrations": "3.
|
|
40
|
+
"@contractspec/lib.contracts-integrations": "3.8.4"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@contractspec/tool.typescript": "3.7.
|
|
43
|
-
"
|
|
44
|
-
"@contractspec/tool.bun": "3.7.6"
|
|
43
|
+
"@contractspec/tool.typescript": "3.7.9",
|
|
44
|
+
"@contractspec/tool.bun": "3.7.9"
|
|
45
45
|
},
|
|
46
46
|
"exports": {
|
|
47
47
|
".": {
|