@contractspec/bundle.workspace 3.7.1 → 3.7.4
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/formatters/json.d.ts +14 -0
- package/dist/index.js +331 -21
- package/dist/node/index.js +331 -21
- package/dist/services/doctor/checks/layers.d.ts +1 -1
- package/dist/services/doctor/checks/layers.test.d.ts +1 -0
- package/dist/services/doctor/checks/package-stability.d.ts +3 -0
- package/dist/services/doctor/checks/package-stability.test.d.ts +1 -0
- package/dist/services/doctor/checks/workspace.test.d.ts +1 -0
- package/dist/services/doctor/types.d.ts +2 -0
- package/dist/services/stability/package-audit.d.ts +25 -0
- package/dist/services/stability/policy.d.ts +12 -0
- package/package.json +11 -11
|
@@ -26,7 +26,17 @@ export interface JsonFormatOptions {
|
|
|
26
26
|
* CI JSON output structure (v1.0).
|
|
27
27
|
*/
|
|
28
28
|
export interface CiJsonOutput extends BaseJsonOutput {
|
|
29
|
+
success: boolean;
|
|
29
30
|
checks: CiCheckJson[];
|
|
31
|
+
categories: {
|
|
32
|
+
category: string;
|
|
33
|
+
label: string;
|
|
34
|
+
passed: boolean;
|
|
35
|
+
errors: number;
|
|
36
|
+
warnings: number;
|
|
37
|
+
notes: number;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
}[];
|
|
30
40
|
drift: {
|
|
31
41
|
status: 'none' | 'detected';
|
|
32
42
|
files: string[];
|
|
@@ -35,7 +45,11 @@ export interface CiJsonOutput extends BaseJsonOutput {
|
|
|
35
45
|
pass: number;
|
|
36
46
|
fail: number;
|
|
37
47
|
warn: number;
|
|
48
|
+
note: number;
|
|
38
49
|
total: number;
|
|
50
|
+
totalErrors: number;
|
|
51
|
+
totalWarnings: number;
|
|
52
|
+
totalNotes: number;
|
|
39
53
|
durationMs: number;
|
|
40
54
|
timestamp: string;
|
|
41
55
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
3
7
|
var __export = (target, all) => {
|
|
4
8
|
for (var name in all)
|
|
5
9
|
__defProp(target, name, {
|
|
6
10
|
get: all[name],
|
|
7
11
|
enumerable: true,
|
|
8
12
|
configurable: true,
|
|
9
|
-
set: (
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
10
14
|
});
|
|
11
15
|
};
|
|
12
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -244,6 +248,8 @@ import { glob as globFn } from "glob";
|
|
|
244
248
|
|
|
245
249
|
// src/adapters/fs.ts
|
|
246
250
|
var DEFAULT_SPEC_PATTERNS = [
|
|
251
|
+
"**/*.command.ts",
|
|
252
|
+
"**/*.query.ts",
|
|
247
253
|
"**/*.operation.ts",
|
|
248
254
|
"**/*.operations.ts",
|
|
249
255
|
"**/*.event.ts",
|
|
@@ -263,6 +269,10 @@ var DEFAULT_SPEC_PATTERNS = [
|
|
|
263
269
|
"**/*.test-spec.ts",
|
|
264
270
|
"**/contracts/*.ts",
|
|
265
271
|
"**/contracts/index.ts",
|
|
272
|
+
"**/commands/*.ts",
|
|
273
|
+
"**/commands/index.ts",
|
|
274
|
+
"**/queries/*.ts",
|
|
275
|
+
"**/queries/index.ts",
|
|
266
276
|
"**/operations/*.ts",
|
|
267
277
|
"**/operations/index.ts",
|
|
268
278
|
"**/operations.ts",
|
|
@@ -3088,9 +3098,7 @@ async function analyzeDeps(adapters, options = {}) {
|
|
|
3088
3098
|
for (const file of files) {
|
|
3089
3099
|
const content = await fs5.readFile(file);
|
|
3090
3100
|
const relativePath = fs5.relative(".", file);
|
|
3091
|
-
const
|
|
3092
|
-
const inferredName = nameMatch?.[1] ? nameMatch[1] : fs5.basename(file).replace(/\.[jt]s$/, "").replace(/\.(contracts|contract|operation|operations|event|presentation|workflow|data-view|migration|telemetry|experiment|app-config|integration|knowledge)$/, "");
|
|
3093
|
-
const finalName = inferredName || "unknown";
|
|
3101
|
+
const finalName = fs5.basename(file).replace(/\.[jt]s$/, "").replace(/\.(contracts|contract|command|query|operation|operations|event|presentation|workflow|data-view|migration|telemetry|experiment|app-config|integration|knowledge)$/, "") || "unknown";
|
|
3094
3102
|
const dependencies = parseImportedSpecNames(content, file);
|
|
3095
3103
|
addContractNode(graph, finalName, relativePath, dependencies);
|
|
3096
3104
|
}
|
|
@@ -6670,6 +6678,271 @@ async function checkContractsLibrary(fs5, ctx) {
|
|
|
6670
6678
|
};
|
|
6671
6679
|
}
|
|
6672
6680
|
}
|
|
6681
|
+
// src/services/stability/policy.ts
|
|
6682
|
+
var STABILITY_POLICY_PATH = "config/stability-policy.json";
|
|
6683
|
+
function normalizePath(value) {
|
|
6684
|
+
return value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
6685
|
+
}
|
|
6686
|
+
function toStringArray(value) {
|
|
6687
|
+
if (!Array.isArray(value))
|
|
6688
|
+
return [];
|
|
6689
|
+
return value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
|
|
6690
|
+
}
|
|
6691
|
+
async function loadStabilityPolicy(fs5, workspaceRoot) {
|
|
6692
|
+
const policyPath = fs5.join(workspaceRoot, STABILITY_POLICY_PATH);
|
|
6693
|
+
if (!await fs5.exists(policyPath)) {
|
|
6694
|
+
return;
|
|
6695
|
+
}
|
|
6696
|
+
try {
|
|
6697
|
+
const content = await fs5.readFile(policyPath);
|
|
6698
|
+
const parsed = JSON.parse(content);
|
|
6699
|
+
return {
|
|
6700
|
+
version: typeof parsed.version === "number" && Number.isFinite(parsed.version) ? parsed.version : 1,
|
|
6701
|
+
criticalPackages: toStringArray(parsed.criticalPackages).map(normalizePath),
|
|
6702
|
+
criticalFeatureKeys: toStringArray(parsed.criticalFeatureKeys),
|
|
6703
|
+
smokePackages: toStringArray(parsed.smokePackages)
|
|
6704
|
+
};
|
|
6705
|
+
} catch {
|
|
6706
|
+
return;
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
function getPackageTier(relativePackagePath, policy) {
|
|
6710
|
+
const normalizedPath = normalizePath(relativePackagePath);
|
|
6711
|
+
if (policy?.criticalPackages.includes(normalizedPath)) {
|
|
6712
|
+
return "critical";
|
|
6713
|
+
}
|
|
6714
|
+
return "non-critical";
|
|
6715
|
+
}
|
|
6716
|
+
function isCriticalFeatureKey(featureKey, policy) {
|
|
6717
|
+
return Boolean(featureKey && policy?.criticalFeatureKeys.includes(featureKey));
|
|
6718
|
+
}
|
|
6719
|
+
function getStabilityPolicyPath(workspaceRoot) {
|
|
6720
|
+
return normalizePath(`${normalizePath(workspaceRoot)}/${STABILITY_POLICY_PATH}`);
|
|
6721
|
+
}
|
|
6722
|
+
|
|
6723
|
+
// src/services/stability/package-audit.ts
|
|
6724
|
+
var TEST_FILE_PATTERNS = ["**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts}"];
|
|
6725
|
+
var TEST_IGNORES = [
|
|
6726
|
+
"**/node_modules/**",
|
|
6727
|
+
"**/dist/**",
|
|
6728
|
+
"**/.next/**",
|
|
6729
|
+
"**/.turbo/**",
|
|
6730
|
+
"**/coverage/**"
|
|
6731
|
+
];
|
|
6732
|
+
function toRecord(value) {
|
|
6733
|
+
if (!value || typeof value !== "object") {
|
|
6734
|
+
return {};
|
|
6735
|
+
}
|
|
6736
|
+
return Object.entries(value).reduce((acc, [key, entry]) => {
|
|
6737
|
+
if (typeof entry === "string") {
|
|
6738
|
+
acc[key] = entry;
|
|
6739
|
+
}
|
|
6740
|
+
return acc;
|
|
6741
|
+
}, {});
|
|
6742
|
+
}
|
|
6743
|
+
function usesPassWithNoTests(scripts) {
|
|
6744
|
+
return Object.entries(scripts).some(([name, command]) => {
|
|
6745
|
+
return name.startsWith("test") && /pass[- ]with[- ]no[- ]tests/i.test(command);
|
|
6746
|
+
});
|
|
6747
|
+
}
|
|
6748
|
+
async function discoverPackages(fs5, workspaceRoot, policy) {
|
|
6749
|
+
const packageJsonFiles = await fs5.glob({
|
|
6750
|
+
pattern: "packages/**/package.json",
|
|
6751
|
+
cwd: workspaceRoot,
|
|
6752
|
+
ignore: TEST_IGNORES
|
|
6753
|
+
});
|
|
6754
|
+
const descriptors = [];
|
|
6755
|
+
for (const packageJsonFile of packageJsonFiles) {
|
|
6756
|
+
const packagePath = fs5.dirname(packageJsonFile);
|
|
6757
|
+
const relativePackagePath = fs5.relative(workspaceRoot, packagePath);
|
|
6758
|
+
try {
|
|
6759
|
+
const packageJson = JSON.parse(await fs5.readFile(packageJsonFile));
|
|
6760
|
+
const scripts = toRecord(packageJson.scripts);
|
|
6761
|
+
const testFiles = await fs5.glob({
|
|
6762
|
+
patterns: TEST_FILE_PATTERNS,
|
|
6763
|
+
cwd: packagePath,
|
|
6764
|
+
ignore: TEST_IGNORES
|
|
6765
|
+
});
|
|
6766
|
+
descriptors.push({
|
|
6767
|
+
packageName: packageJson.name ?? relativePackagePath,
|
|
6768
|
+
packagePath: relativePackagePath.replace(/\\/g, "/"),
|
|
6769
|
+
hasBuildScript: typeof scripts.build === "string",
|
|
6770
|
+
hasTypecheckScript: typeof scripts.typecheck === "string",
|
|
6771
|
+
hasLintScript: typeof scripts.lint === "string" || typeof scripts["lint:check"] === "string",
|
|
6772
|
+
hasTestScript: typeof scripts.test === "string",
|
|
6773
|
+
usesPassWithNoTests: usesPassWithNoTests(scripts),
|
|
6774
|
+
testFileCount: testFiles.length,
|
|
6775
|
+
tier: getPackageTier(relativePackagePath, policy)
|
|
6776
|
+
});
|
|
6777
|
+
} catch {
|
|
6778
|
+
continue;
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
return descriptors;
|
|
6782
|
+
}
|
|
6783
|
+
function createCriticalFindings(descriptor) {
|
|
6784
|
+
const findings = [];
|
|
6785
|
+
if (!descriptor.hasBuildScript) {
|
|
6786
|
+
findings.push({
|
|
6787
|
+
code: "critical-missing-build-script",
|
|
6788
|
+
tier: descriptor.tier,
|
|
6789
|
+
packageName: descriptor.packageName,
|
|
6790
|
+
packagePath: descriptor.packagePath,
|
|
6791
|
+
message: "Missing build script"
|
|
6792
|
+
});
|
|
6793
|
+
}
|
|
6794
|
+
if (!descriptor.hasTypecheckScript) {
|
|
6795
|
+
findings.push({
|
|
6796
|
+
code: "critical-missing-typecheck-script",
|
|
6797
|
+
tier: descriptor.tier,
|
|
6798
|
+
packageName: descriptor.packageName,
|
|
6799
|
+
packagePath: descriptor.packagePath,
|
|
6800
|
+
message: "Missing typecheck script"
|
|
6801
|
+
});
|
|
6802
|
+
}
|
|
6803
|
+
if (!descriptor.hasLintScript) {
|
|
6804
|
+
findings.push({
|
|
6805
|
+
code: "critical-missing-lint-script",
|
|
6806
|
+
tier: descriptor.tier,
|
|
6807
|
+
packageName: descriptor.packageName,
|
|
6808
|
+
packagePath: descriptor.packagePath,
|
|
6809
|
+
message: "Missing lint or lint:check script"
|
|
6810
|
+
});
|
|
6811
|
+
}
|
|
6812
|
+
if (!descriptor.hasTestScript) {
|
|
6813
|
+
findings.push({
|
|
6814
|
+
code: "critical-missing-test-script",
|
|
6815
|
+
tier: descriptor.tier,
|
|
6816
|
+
packageName: descriptor.packageName,
|
|
6817
|
+
packagePath: descriptor.packagePath,
|
|
6818
|
+
message: "Missing test script"
|
|
6819
|
+
});
|
|
6820
|
+
}
|
|
6821
|
+
if (descriptor.testFileCount === 0) {
|
|
6822
|
+
findings.push({
|
|
6823
|
+
code: "critical-missing-test-files",
|
|
6824
|
+
tier: descriptor.tier,
|
|
6825
|
+
packageName: descriptor.packageName,
|
|
6826
|
+
packagePath: descriptor.packagePath,
|
|
6827
|
+
message: "No real test files found"
|
|
6828
|
+
});
|
|
6829
|
+
}
|
|
6830
|
+
if (descriptor.usesPassWithNoTests) {
|
|
6831
|
+
findings.push({
|
|
6832
|
+
code: "critical-pass-with-no-tests",
|
|
6833
|
+
tier: descriptor.tier,
|
|
6834
|
+
packageName: descriptor.packageName,
|
|
6835
|
+
packagePath: descriptor.packagePath,
|
|
6836
|
+
message: "Uses pass-with-no-tests in a critical package"
|
|
6837
|
+
});
|
|
6838
|
+
}
|
|
6839
|
+
return findings;
|
|
6840
|
+
}
|
|
6841
|
+
function createGeneralFindings(descriptor) {
|
|
6842
|
+
const findings = [];
|
|
6843
|
+
if (descriptor.testFileCount > 0 && !descriptor.hasTestScript) {
|
|
6844
|
+
findings.push({
|
|
6845
|
+
code: "tests-without-test-script",
|
|
6846
|
+
tier: descriptor.tier,
|
|
6847
|
+
packageName: descriptor.packageName,
|
|
6848
|
+
packagePath: descriptor.packagePath,
|
|
6849
|
+
message: "Has test files on disk but no test script"
|
|
6850
|
+
});
|
|
6851
|
+
}
|
|
6852
|
+
if (descriptor.hasBuildScript && descriptor.testFileCount === 0 && !descriptor.hasTestScript) {
|
|
6853
|
+
findings.push({
|
|
6854
|
+
code: "build-without-tests",
|
|
6855
|
+
tier: descriptor.tier,
|
|
6856
|
+
packageName: descriptor.packageName,
|
|
6857
|
+
packagePath: descriptor.packagePath,
|
|
6858
|
+
message: "Has a build script but no test script or test files"
|
|
6859
|
+
});
|
|
6860
|
+
}
|
|
6861
|
+
return findings;
|
|
6862
|
+
}
|
|
6863
|
+
async function auditWorkspacePackages(fs5, workspaceRoot, policy) {
|
|
6864
|
+
const packages = await discoverPackages(fs5, workspaceRoot, policy);
|
|
6865
|
+
const findings = [];
|
|
6866
|
+
for (const descriptor of packages) {
|
|
6867
|
+
if (descriptor.tier === "critical") {
|
|
6868
|
+
findings.push(...createCriticalFindings(descriptor));
|
|
6869
|
+
}
|
|
6870
|
+
findings.push(...createGeneralFindings(descriptor));
|
|
6871
|
+
}
|
|
6872
|
+
const criticalPackages = packages.filter((descriptor) => descriptor.tier === "critical").map((descriptor) => ({
|
|
6873
|
+
packageName: descriptor.packageName,
|
|
6874
|
+
packagePath: descriptor.packagePath,
|
|
6875
|
+
hasBuildScript: descriptor.hasBuildScript,
|
|
6876
|
+
hasTypecheckScript: descriptor.hasTypecheckScript,
|
|
6877
|
+
hasLintScript: descriptor.hasLintScript,
|
|
6878
|
+
hasTestScript: descriptor.hasTestScript,
|
|
6879
|
+
usesPassWithNoTests: descriptor.usesPassWithNoTests,
|
|
6880
|
+
testFileCount: descriptor.testFileCount
|
|
6881
|
+
}));
|
|
6882
|
+
return { findings, criticalPackages };
|
|
6883
|
+
}
|
|
6884
|
+
|
|
6885
|
+
// src/services/doctor/checks/package-stability.ts
|
|
6886
|
+
function summarizeFindings(findings) {
|
|
6887
|
+
return findings.slice(0, 5).map((finding) => `${finding.packagePath}: ${finding.message}`).join("; ");
|
|
6888
|
+
}
|
|
6889
|
+
function createCheckResult(name, findings, failureCodes, successMessage) {
|
|
6890
|
+
if (findings.length === 0) {
|
|
6891
|
+
return {
|
|
6892
|
+
category: "workspace",
|
|
6893
|
+
name,
|
|
6894
|
+
status: "pass",
|
|
6895
|
+
message: successMessage,
|
|
6896
|
+
context: { findings: [] }
|
|
6897
|
+
};
|
|
6898
|
+
}
|
|
6899
|
+
const failingFindings = findings.filter((finding) => {
|
|
6900
|
+
return finding.tier === "critical" || failureCodes.has(finding.code);
|
|
6901
|
+
});
|
|
6902
|
+
return {
|
|
6903
|
+
category: "workspace",
|
|
6904
|
+
name,
|
|
6905
|
+
status: failingFindings.length > 0 ? "fail" : "warn",
|
|
6906
|
+
message: `${findings.length} package issue(s) found`,
|
|
6907
|
+
details: summarizeFindings(findings),
|
|
6908
|
+
context: { findings }
|
|
6909
|
+
};
|
|
6910
|
+
}
|
|
6911
|
+
async function runPackageStabilityChecks(fs5, ctx) {
|
|
6912
|
+
const policy = await loadStabilityPolicy(fs5, ctx.workspaceRoot);
|
|
6913
|
+
if (!policy) {
|
|
6914
|
+
return [];
|
|
6915
|
+
}
|
|
6916
|
+
const report = await auditWorkspacePackages(fs5, ctx.workspaceRoot, policy);
|
|
6917
|
+
const criticalPackageCodes = new Set([
|
|
6918
|
+
"critical-missing-build-script",
|
|
6919
|
+
"critical-missing-typecheck-script",
|
|
6920
|
+
"critical-missing-lint-script",
|
|
6921
|
+
"critical-missing-test-script",
|
|
6922
|
+
"critical-missing-test-files",
|
|
6923
|
+
"critical-pass-with-no-tests"
|
|
6924
|
+
]);
|
|
6925
|
+
const qualityGateFindings = report.findings.filter((finding) => criticalPackageCodes.has(finding.code));
|
|
6926
|
+
const testsWithoutScript = report.findings.filter((finding) => finding.code === "tests-without-test-script");
|
|
6927
|
+
const buildWithoutTests = report.findings.filter((finding) => finding.code === "build-without-tests");
|
|
6928
|
+
return [
|
|
6929
|
+
{
|
|
6930
|
+
category: "workspace",
|
|
6931
|
+
name: "Critical Package Gates",
|
|
6932
|
+
status: qualityGateFindings.length > 0 ? "fail" : "pass",
|
|
6933
|
+
message: qualityGateFindings.length > 0 ? `${qualityGateFindings.length} critical package gate failure(s)` : `All ${report.criticalPackages.length} critical packages meet build, lint, typecheck, and test requirements`,
|
|
6934
|
+
details: qualityGateFindings.length > 0 ? summarizeFindings(qualityGateFindings) : ctx.verbose ? `Policy: ${getStabilityPolicyPath(ctx.workspaceRoot)}` : undefined,
|
|
6935
|
+
context: {
|
|
6936
|
+
policyPath: getStabilityPolicyPath(ctx.workspaceRoot),
|
|
6937
|
+
criticalPackages: report.criticalPackages,
|
|
6938
|
+
findings: qualityGateFindings
|
|
6939
|
+
}
|
|
6940
|
+
},
|
|
6941
|
+
createCheckResult("Package Test Scripts", testsWithoutScript, new Set, "All tested packages expose a test script"),
|
|
6942
|
+
createCheckResult("Buildable Packages Without Tests", buildWithoutTests, new Set, "All buildable packages have tests or explicit test scripts")
|
|
6943
|
+
];
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6673
6946
|
// src/services/doctor/checks/workspace.ts
|
|
6674
6947
|
var CONTRACT_PATHS = ["src/contracts", "contracts", "src/specs", "specs"];
|
|
6675
6948
|
async function runWorkspaceChecks(fs5, ctx) {
|
|
@@ -6679,6 +6952,7 @@ async function runWorkspaceChecks(fs5, ctx) {
|
|
|
6679
6952
|
results.push(await checkContractsDirectory(fs5, ctx));
|
|
6680
6953
|
results.push(await checkContractFiles(fs5, ctx));
|
|
6681
6954
|
results.push(await checkOutputDirectory(fs5, ctx));
|
|
6955
|
+
results.push(...await runPackageStabilityChecks(fs5, ctx));
|
|
6682
6956
|
return results;
|
|
6683
6957
|
}
|
|
6684
6958
|
function checkMonorepoStatus(ctx) {
|
|
@@ -6860,12 +7134,15 @@ async function checkOutputDirectory(fs5, ctx) {
|
|
|
6860
7134
|
details: ctx.verbose ? `Resolved to: ${outputPath}` : undefined
|
|
6861
7135
|
};
|
|
6862
7136
|
}
|
|
6863
|
-
|
|
7137
|
+
const isDefaultWorkspaceOutput = outputDir === "./src" || outputDir === "src";
|
|
7138
|
+
const usesPackageScopedConfig = Array.isArray(config.packages) && config.packages.length > 0;
|
|
7139
|
+
if (ctx.isMonorepo && ctx.packageRoot === ctx.workspaceRoot && isDefaultWorkspaceOutput && (usesPackageScopedConfig || configInfo.level === "workspace")) {
|
|
6864
7140
|
return {
|
|
6865
7141
|
category: "workspace",
|
|
6866
7142
|
name: "Output Directory",
|
|
6867
7143
|
status: "pass",
|
|
6868
|
-
message: "Monorepo root detected (using package directories)"
|
|
7144
|
+
message: "Monorepo root detected (using package directories)",
|
|
7145
|
+
details: ctx.verbose ? `Resolved default output to packages via ${configInfo.path}` : undefined
|
|
6869
7146
|
};
|
|
6870
7147
|
}
|
|
6871
7148
|
return {
|
|
@@ -7018,8 +7295,9 @@ async function checkApiKey(fs5, ctx, prompts) {
|
|
|
7018
7295
|
}
|
|
7019
7296
|
}
|
|
7020
7297
|
// src/services/doctor/checks/layers.ts
|
|
7021
|
-
async function runLayerChecks(fs5,
|
|
7298
|
+
async function runLayerChecks(fs5, ctx) {
|
|
7022
7299
|
const results = [];
|
|
7300
|
+
const policy = await loadStabilityPolicy(fs5, ctx.workspaceRoot);
|
|
7023
7301
|
const discovery = await discoverLayers({
|
|
7024
7302
|
fs: fs5,
|
|
7025
7303
|
logger: {
|
|
@@ -7040,7 +7318,7 @@ async function runLayerChecks(fs5, _ctx) {
|
|
|
7040
7318
|
}, {});
|
|
7041
7319
|
results.push(checkHasFeatures(discovery.stats.features));
|
|
7042
7320
|
results.push(checkHasExamples(discovery.stats.examples));
|
|
7043
|
-
results.push(checkFeatureOwners(discovery.inventory.features));
|
|
7321
|
+
results.push(checkFeatureOwners(discovery.inventory.features, policy, ctx));
|
|
7044
7322
|
results.push(checkExampleEntrypoints(discovery.inventory.examples));
|
|
7045
7323
|
results.push(checkWorkspaceConfigs(discovery.inventory.workspaceConfigs));
|
|
7046
7324
|
return results;
|
|
@@ -7079,27 +7357,43 @@ function checkHasExamples(count) {
|
|
|
7079
7357
|
details: "Create an example.ts file to package reusable templates"
|
|
7080
7358
|
};
|
|
7081
7359
|
}
|
|
7082
|
-
function checkFeatureOwners(features) {
|
|
7360
|
+
function checkFeatureOwners(features, policy, ctx) {
|
|
7083
7361
|
const missingOwners = [];
|
|
7362
|
+
const criticalMissingOwners = [];
|
|
7084
7363
|
for (const [key, feature] of features) {
|
|
7085
|
-
|
|
7086
|
-
|
|
7364
|
+
const hasOwnerMetadata = Boolean(feature.owners?.length) || /owners\s*:\s*(?!\[\s*\])/.test(feature.sourceBlock ?? "");
|
|
7365
|
+
if (!hasOwnerMetadata) {
|
|
7366
|
+
if (isCriticalFeatureKey(key, policy)) {
|
|
7367
|
+
criticalMissingOwners.push(key);
|
|
7368
|
+
} else {
|
|
7369
|
+
missingOwners.push(key);
|
|
7370
|
+
}
|
|
7087
7371
|
}
|
|
7088
7372
|
}
|
|
7089
|
-
if (missingOwners.length === 0) {
|
|
7373
|
+
if (criticalMissingOwners.length === 0 && missingOwners.length === 0) {
|
|
7090
7374
|
return {
|
|
7091
7375
|
category: "layers",
|
|
7092
7376
|
name: "Feature Owners",
|
|
7093
7377
|
status: features.size > 0 ? "pass" : "skip",
|
|
7094
|
-
message: features.size > 0 ? "All features have owners defined" : "No features to check"
|
|
7378
|
+
message: features.size > 0 ? "All features have owners defined" : "No features to check",
|
|
7379
|
+
context: features.size > 0 ? {
|
|
7380
|
+
policyPath: policy ? getStabilityPolicyPath(ctx.workspaceRoot) : undefined,
|
|
7381
|
+
criticalMissingFeatures: [],
|
|
7382
|
+
missingFeatures: []
|
|
7383
|
+
} : undefined
|
|
7095
7384
|
};
|
|
7096
7385
|
}
|
|
7097
7386
|
return {
|
|
7098
7387
|
category: "layers",
|
|
7099
7388
|
name: "Feature Owners",
|
|
7100
|
-
status: "warn",
|
|
7101
|
-
message: `${missingOwners.length} feature(s) missing owners`,
|
|
7102
|
-
details: `Features: ${missingOwners.slice(0, 3).join(", ")}${missingOwners.length > 3 ? "..." : ""}
|
|
7389
|
+
status: criticalMissingOwners.length > 0 ? "fail" : "warn",
|
|
7390
|
+
message: criticalMissingOwners.length > 0 ? `${criticalMissingOwners.length} critical feature(s) missing owners` : `${missingOwners.length} feature(s) missing owners`,
|
|
7391
|
+
details: criticalMissingOwners.length > 0 ? `Critical features: ${criticalMissingOwners.join(", ")}` : `Features: ${missingOwners.slice(0, 3).join(", ")}${missingOwners.length > 3 ? "..." : ""}`,
|
|
7392
|
+
context: {
|
|
7393
|
+
policyPath: policy ? getStabilityPolicyPath(ctx.workspaceRoot) : undefined,
|
|
7394
|
+
criticalMissingFeatures: criticalMissingOwners,
|
|
7395
|
+
missingFeatures: missingOwners
|
|
7396
|
+
}
|
|
7103
7397
|
};
|
|
7104
7398
|
}
|
|
7105
7399
|
function checkExampleEntrypoints(examples) {
|
|
@@ -7636,7 +7930,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7636
7930
|
const result = await runDoctor(adapters, {
|
|
7637
7931
|
workspaceRoot,
|
|
7638
7932
|
skipAi: true,
|
|
7639
|
-
categories: ["cli", "config", "deps", "workspace"]
|
|
7933
|
+
categories: ["cli", "config", "deps", "workspace", "layers"]
|
|
7640
7934
|
});
|
|
7641
7935
|
for (const check of result.checks) {
|
|
7642
7936
|
if (check.status === "fail") {
|
|
@@ -7645,7 +7939,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7645
7939
|
severity: "error",
|
|
7646
7940
|
message: `${check.name}: ${check.message}`,
|
|
7647
7941
|
category: "doctor",
|
|
7648
|
-
context: { details: check.details }
|
|
7942
|
+
context: { details: check.details, ...check.context ?? {} }
|
|
7649
7943
|
});
|
|
7650
7944
|
} else if (check.status === "warn") {
|
|
7651
7945
|
issues.push({
|
|
@@ -7653,7 +7947,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7653
7947
|
severity: "warning",
|
|
7654
7948
|
message: `${check.name}: ${check.message}`,
|
|
7655
7949
|
category: "doctor",
|
|
7656
|
-
context: { details: check.details }
|
|
7950
|
+
context: { details: check.details, ...check.context ?? {} }
|
|
7657
7951
|
});
|
|
7658
7952
|
}
|
|
7659
7953
|
}
|
|
@@ -16851,20 +17145,36 @@ function formatAsJson(result, options = {}) {
|
|
|
16851
17145
|
line: issue.line,
|
|
16852
17146
|
details: issue.context
|
|
16853
17147
|
}));
|
|
17148
|
+
const pass = result.categories.filter((category) => category.passed).length;
|
|
16854
17149
|
const fail = result.totalErrors;
|
|
16855
17150
|
const warn = result.totalWarnings;
|
|
17151
|
+
const note = result.totalNotes;
|
|
16856
17152
|
const output = {
|
|
16857
17153
|
schemaVersion: "1.0",
|
|
17154
|
+
success: result.success,
|
|
16858
17155
|
checks,
|
|
17156
|
+
categories: result.categories.map((category) => ({
|
|
17157
|
+
category: category.category,
|
|
17158
|
+
label: category.label,
|
|
17159
|
+
passed: category.passed,
|
|
17160
|
+
errors: category.errors,
|
|
17161
|
+
warnings: category.warnings,
|
|
17162
|
+
notes: category.notes,
|
|
17163
|
+
durationMs: category.durationMs
|
|
17164
|
+
})),
|
|
16859
17165
|
drift: {
|
|
16860
17166
|
status: driftResult?.hasDrift ? "detected" : "none",
|
|
16861
17167
|
files: driftResult?.files ?? []
|
|
16862
17168
|
},
|
|
16863
17169
|
summary: {
|
|
16864
|
-
pass
|
|
17170
|
+
pass,
|
|
16865
17171
|
fail,
|
|
16866
17172
|
warn,
|
|
16867
|
-
|
|
17173
|
+
note,
|
|
17174
|
+
total: pass + fail + warn + note,
|
|
17175
|
+
totalErrors: result.totalErrors,
|
|
17176
|
+
totalWarnings: result.totalWarnings,
|
|
17177
|
+
totalNotes: result.totalNotes,
|
|
16868
17178
|
durationMs: result.durationMs,
|
|
16869
17179
|
timestamp: result.timestamp
|
|
16870
17180
|
},
|
package/dist/node/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
3
7
|
var __export = (target, all) => {
|
|
4
8
|
for (var name in all)
|
|
5
9
|
__defProp(target, name, {
|
|
6
10
|
get: all[name],
|
|
7
11
|
enumerable: true,
|
|
8
12
|
configurable: true,
|
|
9
|
-
set: (
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
10
14
|
});
|
|
11
15
|
};
|
|
12
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -244,6 +248,8 @@ import { glob as globFn } from "glob";
|
|
|
244
248
|
|
|
245
249
|
// src/adapters/fs.ts
|
|
246
250
|
var DEFAULT_SPEC_PATTERNS = [
|
|
251
|
+
"**/*.command.ts",
|
|
252
|
+
"**/*.query.ts",
|
|
247
253
|
"**/*.operation.ts",
|
|
248
254
|
"**/*.operations.ts",
|
|
249
255
|
"**/*.event.ts",
|
|
@@ -263,6 +269,10 @@ var DEFAULT_SPEC_PATTERNS = [
|
|
|
263
269
|
"**/*.test-spec.ts",
|
|
264
270
|
"**/contracts/*.ts",
|
|
265
271
|
"**/contracts/index.ts",
|
|
272
|
+
"**/commands/*.ts",
|
|
273
|
+
"**/commands/index.ts",
|
|
274
|
+
"**/queries/*.ts",
|
|
275
|
+
"**/queries/index.ts",
|
|
266
276
|
"**/operations/*.ts",
|
|
267
277
|
"**/operations/index.ts",
|
|
268
278
|
"**/operations.ts",
|
|
@@ -3088,9 +3098,7 @@ async function analyzeDeps(adapters, options = {}) {
|
|
|
3088
3098
|
for (const file of files) {
|
|
3089
3099
|
const content = await fs5.readFile(file);
|
|
3090
3100
|
const relativePath = fs5.relative(".", file);
|
|
3091
|
-
const
|
|
3092
|
-
const inferredName = nameMatch?.[1] ? nameMatch[1] : fs5.basename(file).replace(/\.[jt]s$/, "").replace(/\.(contracts|contract|operation|operations|event|presentation|workflow|data-view|migration|telemetry|experiment|app-config|integration|knowledge)$/, "");
|
|
3093
|
-
const finalName = inferredName || "unknown";
|
|
3101
|
+
const finalName = fs5.basename(file).replace(/\.[jt]s$/, "").replace(/\.(contracts|contract|command|query|operation|operations|event|presentation|workflow|data-view|migration|telemetry|experiment|app-config|integration|knowledge)$/, "") || "unknown";
|
|
3094
3102
|
const dependencies = parseImportedSpecNames(content, file);
|
|
3095
3103
|
addContractNode(graph, finalName, relativePath, dependencies);
|
|
3096
3104
|
}
|
|
@@ -6670,6 +6678,271 @@ async function checkContractsLibrary(fs5, ctx) {
|
|
|
6670
6678
|
};
|
|
6671
6679
|
}
|
|
6672
6680
|
}
|
|
6681
|
+
// src/services/stability/policy.ts
|
|
6682
|
+
var STABILITY_POLICY_PATH = "config/stability-policy.json";
|
|
6683
|
+
function normalizePath(value) {
|
|
6684
|
+
return value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
6685
|
+
}
|
|
6686
|
+
function toStringArray(value) {
|
|
6687
|
+
if (!Array.isArray(value))
|
|
6688
|
+
return [];
|
|
6689
|
+
return value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
|
|
6690
|
+
}
|
|
6691
|
+
async function loadStabilityPolicy(fs5, workspaceRoot) {
|
|
6692
|
+
const policyPath = fs5.join(workspaceRoot, STABILITY_POLICY_PATH);
|
|
6693
|
+
if (!await fs5.exists(policyPath)) {
|
|
6694
|
+
return;
|
|
6695
|
+
}
|
|
6696
|
+
try {
|
|
6697
|
+
const content = await fs5.readFile(policyPath);
|
|
6698
|
+
const parsed = JSON.parse(content);
|
|
6699
|
+
return {
|
|
6700
|
+
version: typeof parsed.version === "number" && Number.isFinite(parsed.version) ? parsed.version : 1,
|
|
6701
|
+
criticalPackages: toStringArray(parsed.criticalPackages).map(normalizePath),
|
|
6702
|
+
criticalFeatureKeys: toStringArray(parsed.criticalFeatureKeys),
|
|
6703
|
+
smokePackages: toStringArray(parsed.smokePackages)
|
|
6704
|
+
};
|
|
6705
|
+
} catch {
|
|
6706
|
+
return;
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
function getPackageTier(relativePackagePath, policy) {
|
|
6710
|
+
const normalizedPath = normalizePath(relativePackagePath);
|
|
6711
|
+
if (policy?.criticalPackages.includes(normalizedPath)) {
|
|
6712
|
+
return "critical";
|
|
6713
|
+
}
|
|
6714
|
+
return "non-critical";
|
|
6715
|
+
}
|
|
6716
|
+
function isCriticalFeatureKey(featureKey, policy) {
|
|
6717
|
+
return Boolean(featureKey && policy?.criticalFeatureKeys.includes(featureKey));
|
|
6718
|
+
}
|
|
6719
|
+
function getStabilityPolicyPath(workspaceRoot) {
|
|
6720
|
+
return normalizePath(`${normalizePath(workspaceRoot)}/${STABILITY_POLICY_PATH}`);
|
|
6721
|
+
}
|
|
6722
|
+
|
|
6723
|
+
// src/services/stability/package-audit.ts
|
|
6724
|
+
var TEST_FILE_PATTERNS = ["**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts}"];
|
|
6725
|
+
var TEST_IGNORES = [
|
|
6726
|
+
"**/node_modules/**",
|
|
6727
|
+
"**/dist/**",
|
|
6728
|
+
"**/.next/**",
|
|
6729
|
+
"**/.turbo/**",
|
|
6730
|
+
"**/coverage/**"
|
|
6731
|
+
];
|
|
6732
|
+
function toRecord(value) {
|
|
6733
|
+
if (!value || typeof value !== "object") {
|
|
6734
|
+
return {};
|
|
6735
|
+
}
|
|
6736
|
+
return Object.entries(value).reduce((acc, [key, entry]) => {
|
|
6737
|
+
if (typeof entry === "string") {
|
|
6738
|
+
acc[key] = entry;
|
|
6739
|
+
}
|
|
6740
|
+
return acc;
|
|
6741
|
+
}, {});
|
|
6742
|
+
}
|
|
6743
|
+
function usesPassWithNoTests(scripts) {
|
|
6744
|
+
return Object.entries(scripts).some(([name, command]) => {
|
|
6745
|
+
return name.startsWith("test") && /pass[- ]with[- ]no[- ]tests/i.test(command);
|
|
6746
|
+
});
|
|
6747
|
+
}
|
|
6748
|
+
async function discoverPackages(fs5, workspaceRoot, policy) {
|
|
6749
|
+
const packageJsonFiles = await fs5.glob({
|
|
6750
|
+
pattern: "packages/**/package.json",
|
|
6751
|
+
cwd: workspaceRoot,
|
|
6752
|
+
ignore: TEST_IGNORES
|
|
6753
|
+
});
|
|
6754
|
+
const descriptors = [];
|
|
6755
|
+
for (const packageJsonFile of packageJsonFiles) {
|
|
6756
|
+
const packagePath = fs5.dirname(packageJsonFile);
|
|
6757
|
+
const relativePackagePath = fs5.relative(workspaceRoot, packagePath);
|
|
6758
|
+
try {
|
|
6759
|
+
const packageJson = JSON.parse(await fs5.readFile(packageJsonFile));
|
|
6760
|
+
const scripts = toRecord(packageJson.scripts);
|
|
6761
|
+
const testFiles = await fs5.glob({
|
|
6762
|
+
patterns: TEST_FILE_PATTERNS,
|
|
6763
|
+
cwd: packagePath,
|
|
6764
|
+
ignore: TEST_IGNORES
|
|
6765
|
+
});
|
|
6766
|
+
descriptors.push({
|
|
6767
|
+
packageName: packageJson.name ?? relativePackagePath,
|
|
6768
|
+
packagePath: relativePackagePath.replace(/\\/g, "/"),
|
|
6769
|
+
hasBuildScript: typeof scripts.build === "string",
|
|
6770
|
+
hasTypecheckScript: typeof scripts.typecheck === "string",
|
|
6771
|
+
hasLintScript: typeof scripts.lint === "string" || typeof scripts["lint:check"] === "string",
|
|
6772
|
+
hasTestScript: typeof scripts.test === "string",
|
|
6773
|
+
usesPassWithNoTests: usesPassWithNoTests(scripts),
|
|
6774
|
+
testFileCount: testFiles.length,
|
|
6775
|
+
tier: getPackageTier(relativePackagePath, policy)
|
|
6776
|
+
});
|
|
6777
|
+
} catch {
|
|
6778
|
+
continue;
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
return descriptors;
|
|
6782
|
+
}
|
|
6783
|
+
function createCriticalFindings(descriptor) {
|
|
6784
|
+
const findings = [];
|
|
6785
|
+
if (!descriptor.hasBuildScript) {
|
|
6786
|
+
findings.push({
|
|
6787
|
+
code: "critical-missing-build-script",
|
|
6788
|
+
tier: descriptor.tier,
|
|
6789
|
+
packageName: descriptor.packageName,
|
|
6790
|
+
packagePath: descriptor.packagePath,
|
|
6791
|
+
message: "Missing build script"
|
|
6792
|
+
});
|
|
6793
|
+
}
|
|
6794
|
+
if (!descriptor.hasTypecheckScript) {
|
|
6795
|
+
findings.push({
|
|
6796
|
+
code: "critical-missing-typecheck-script",
|
|
6797
|
+
tier: descriptor.tier,
|
|
6798
|
+
packageName: descriptor.packageName,
|
|
6799
|
+
packagePath: descriptor.packagePath,
|
|
6800
|
+
message: "Missing typecheck script"
|
|
6801
|
+
});
|
|
6802
|
+
}
|
|
6803
|
+
if (!descriptor.hasLintScript) {
|
|
6804
|
+
findings.push({
|
|
6805
|
+
code: "critical-missing-lint-script",
|
|
6806
|
+
tier: descriptor.tier,
|
|
6807
|
+
packageName: descriptor.packageName,
|
|
6808
|
+
packagePath: descriptor.packagePath,
|
|
6809
|
+
message: "Missing lint or lint:check script"
|
|
6810
|
+
});
|
|
6811
|
+
}
|
|
6812
|
+
if (!descriptor.hasTestScript) {
|
|
6813
|
+
findings.push({
|
|
6814
|
+
code: "critical-missing-test-script",
|
|
6815
|
+
tier: descriptor.tier,
|
|
6816
|
+
packageName: descriptor.packageName,
|
|
6817
|
+
packagePath: descriptor.packagePath,
|
|
6818
|
+
message: "Missing test script"
|
|
6819
|
+
});
|
|
6820
|
+
}
|
|
6821
|
+
if (descriptor.testFileCount === 0) {
|
|
6822
|
+
findings.push({
|
|
6823
|
+
code: "critical-missing-test-files",
|
|
6824
|
+
tier: descriptor.tier,
|
|
6825
|
+
packageName: descriptor.packageName,
|
|
6826
|
+
packagePath: descriptor.packagePath,
|
|
6827
|
+
message: "No real test files found"
|
|
6828
|
+
});
|
|
6829
|
+
}
|
|
6830
|
+
if (descriptor.usesPassWithNoTests) {
|
|
6831
|
+
findings.push({
|
|
6832
|
+
code: "critical-pass-with-no-tests",
|
|
6833
|
+
tier: descriptor.tier,
|
|
6834
|
+
packageName: descriptor.packageName,
|
|
6835
|
+
packagePath: descriptor.packagePath,
|
|
6836
|
+
message: "Uses pass-with-no-tests in a critical package"
|
|
6837
|
+
});
|
|
6838
|
+
}
|
|
6839
|
+
return findings;
|
|
6840
|
+
}
|
|
6841
|
+
function createGeneralFindings(descriptor) {
|
|
6842
|
+
const findings = [];
|
|
6843
|
+
if (descriptor.testFileCount > 0 && !descriptor.hasTestScript) {
|
|
6844
|
+
findings.push({
|
|
6845
|
+
code: "tests-without-test-script",
|
|
6846
|
+
tier: descriptor.tier,
|
|
6847
|
+
packageName: descriptor.packageName,
|
|
6848
|
+
packagePath: descriptor.packagePath,
|
|
6849
|
+
message: "Has test files on disk but no test script"
|
|
6850
|
+
});
|
|
6851
|
+
}
|
|
6852
|
+
if (descriptor.hasBuildScript && descriptor.testFileCount === 0 && !descriptor.hasTestScript) {
|
|
6853
|
+
findings.push({
|
|
6854
|
+
code: "build-without-tests",
|
|
6855
|
+
tier: descriptor.tier,
|
|
6856
|
+
packageName: descriptor.packageName,
|
|
6857
|
+
packagePath: descriptor.packagePath,
|
|
6858
|
+
message: "Has a build script but no test script or test files"
|
|
6859
|
+
});
|
|
6860
|
+
}
|
|
6861
|
+
return findings;
|
|
6862
|
+
}
|
|
6863
|
+
async function auditWorkspacePackages(fs5, workspaceRoot, policy) {
|
|
6864
|
+
const packages = await discoverPackages(fs5, workspaceRoot, policy);
|
|
6865
|
+
const findings = [];
|
|
6866
|
+
for (const descriptor of packages) {
|
|
6867
|
+
if (descriptor.tier === "critical") {
|
|
6868
|
+
findings.push(...createCriticalFindings(descriptor));
|
|
6869
|
+
}
|
|
6870
|
+
findings.push(...createGeneralFindings(descriptor));
|
|
6871
|
+
}
|
|
6872
|
+
const criticalPackages = packages.filter((descriptor) => descriptor.tier === "critical").map((descriptor) => ({
|
|
6873
|
+
packageName: descriptor.packageName,
|
|
6874
|
+
packagePath: descriptor.packagePath,
|
|
6875
|
+
hasBuildScript: descriptor.hasBuildScript,
|
|
6876
|
+
hasTypecheckScript: descriptor.hasTypecheckScript,
|
|
6877
|
+
hasLintScript: descriptor.hasLintScript,
|
|
6878
|
+
hasTestScript: descriptor.hasTestScript,
|
|
6879
|
+
usesPassWithNoTests: descriptor.usesPassWithNoTests,
|
|
6880
|
+
testFileCount: descriptor.testFileCount
|
|
6881
|
+
}));
|
|
6882
|
+
return { findings, criticalPackages };
|
|
6883
|
+
}
|
|
6884
|
+
|
|
6885
|
+
// src/services/doctor/checks/package-stability.ts
|
|
6886
|
+
function summarizeFindings(findings) {
|
|
6887
|
+
return findings.slice(0, 5).map((finding) => `${finding.packagePath}: ${finding.message}`).join("; ");
|
|
6888
|
+
}
|
|
6889
|
+
function createCheckResult(name, findings, failureCodes, successMessage) {
|
|
6890
|
+
if (findings.length === 0) {
|
|
6891
|
+
return {
|
|
6892
|
+
category: "workspace",
|
|
6893
|
+
name,
|
|
6894
|
+
status: "pass",
|
|
6895
|
+
message: successMessage,
|
|
6896
|
+
context: { findings: [] }
|
|
6897
|
+
};
|
|
6898
|
+
}
|
|
6899
|
+
const failingFindings = findings.filter((finding) => {
|
|
6900
|
+
return finding.tier === "critical" || failureCodes.has(finding.code);
|
|
6901
|
+
});
|
|
6902
|
+
return {
|
|
6903
|
+
category: "workspace",
|
|
6904
|
+
name,
|
|
6905
|
+
status: failingFindings.length > 0 ? "fail" : "warn",
|
|
6906
|
+
message: `${findings.length} package issue(s) found`,
|
|
6907
|
+
details: summarizeFindings(findings),
|
|
6908
|
+
context: { findings }
|
|
6909
|
+
};
|
|
6910
|
+
}
|
|
6911
|
+
async function runPackageStabilityChecks(fs5, ctx) {
|
|
6912
|
+
const policy = await loadStabilityPolicy(fs5, ctx.workspaceRoot);
|
|
6913
|
+
if (!policy) {
|
|
6914
|
+
return [];
|
|
6915
|
+
}
|
|
6916
|
+
const report = await auditWorkspacePackages(fs5, ctx.workspaceRoot, policy);
|
|
6917
|
+
const criticalPackageCodes = new Set([
|
|
6918
|
+
"critical-missing-build-script",
|
|
6919
|
+
"critical-missing-typecheck-script",
|
|
6920
|
+
"critical-missing-lint-script",
|
|
6921
|
+
"critical-missing-test-script",
|
|
6922
|
+
"critical-missing-test-files",
|
|
6923
|
+
"critical-pass-with-no-tests"
|
|
6924
|
+
]);
|
|
6925
|
+
const qualityGateFindings = report.findings.filter((finding) => criticalPackageCodes.has(finding.code));
|
|
6926
|
+
const testsWithoutScript = report.findings.filter((finding) => finding.code === "tests-without-test-script");
|
|
6927
|
+
const buildWithoutTests = report.findings.filter((finding) => finding.code === "build-without-tests");
|
|
6928
|
+
return [
|
|
6929
|
+
{
|
|
6930
|
+
category: "workspace",
|
|
6931
|
+
name: "Critical Package Gates",
|
|
6932
|
+
status: qualityGateFindings.length > 0 ? "fail" : "pass",
|
|
6933
|
+
message: qualityGateFindings.length > 0 ? `${qualityGateFindings.length} critical package gate failure(s)` : `All ${report.criticalPackages.length} critical packages meet build, lint, typecheck, and test requirements`,
|
|
6934
|
+
details: qualityGateFindings.length > 0 ? summarizeFindings(qualityGateFindings) : ctx.verbose ? `Policy: ${getStabilityPolicyPath(ctx.workspaceRoot)}` : undefined,
|
|
6935
|
+
context: {
|
|
6936
|
+
policyPath: getStabilityPolicyPath(ctx.workspaceRoot),
|
|
6937
|
+
criticalPackages: report.criticalPackages,
|
|
6938
|
+
findings: qualityGateFindings
|
|
6939
|
+
}
|
|
6940
|
+
},
|
|
6941
|
+
createCheckResult("Package Test Scripts", testsWithoutScript, new Set, "All tested packages expose a test script"),
|
|
6942
|
+
createCheckResult("Buildable Packages Without Tests", buildWithoutTests, new Set, "All buildable packages have tests or explicit test scripts")
|
|
6943
|
+
];
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6673
6946
|
// src/services/doctor/checks/workspace.ts
|
|
6674
6947
|
var CONTRACT_PATHS = ["src/contracts", "contracts", "src/specs", "specs"];
|
|
6675
6948
|
async function runWorkspaceChecks(fs5, ctx) {
|
|
@@ -6679,6 +6952,7 @@ async function runWorkspaceChecks(fs5, ctx) {
|
|
|
6679
6952
|
results.push(await checkContractsDirectory(fs5, ctx));
|
|
6680
6953
|
results.push(await checkContractFiles(fs5, ctx));
|
|
6681
6954
|
results.push(await checkOutputDirectory(fs5, ctx));
|
|
6955
|
+
results.push(...await runPackageStabilityChecks(fs5, ctx));
|
|
6682
6956
|
return results;
|
|
6683
6957
|
}
|
|
6684
6958
|
function checkMonorepoStatus(ctx) {
|
|
@@ -6860,12 +7134,15 @@ async function checkOutputDirectory(fs5, ctx) {
|
|
|
6860
7134
|
details: ctx.verbose ? `Resolved to: ${outputPath}` : undefined
|
|
6861
7135
|
};
|
|
6862
7136
|
}
|
|
6863
|
-
|
|
7137
|
+
const isDefaultWorkspaceOutput = outputDir === "./src" || outputDir === "src";
|
|
7138
|
+
const usesPackageScopedConfig = Array.isArray(config.packages) && config.packages.length > 0;
|
|
7139
|
+
if (ctx.isMonorepo && ctx.packageRoot === ctx.workspaceRoot && isDefaultWorkspaceOutput && (usesPackageScopedConfig || configInfo.level === "workspace")) {
|
|
6864
7140
|
return {
|
|
6865
7141
|
category: "workspace",
|
|
6866
7142
|
name: "Output Directory",
|
|
6867
7143
|
status: "pass",
|
|
6868
|
-
message: "Monorepo root detected (using package directories)"
|
|
7144
|
+
message: "Monorepo root detected (using package directories)",
|
|
7145
|
+
details: ctx.verbose ? `Resolved default output to packages via ${configInfo.path}` : undefined
|
|
6869
7146
|
};
|
|
6870
7147
|
}
|
|
6871
7148
|
return {
|
|
@@ -7018,8 +7295,9 @@ async function checkApiKey(fs5, ctx, prompts) {
|
|
|
7018
7295
|
}
|
|
7019
7296
|
}
|
|
7020
7297
|
// src/services/doctor/checks/layers.ts
|
|
7021
|
-
async function runLayerChecks(fs5,
|
|
7298
|
+
async function runLayerChecks(fs5, ctx) {
|
|
7022
7299
|
const results = [];
|
|
7300
|
+
const policy = await loadStabilityPolicy(fs5, ctx.workspaceRoot);
|
|
7023
7301
|
const discovery = await discoverLayers({
|
|
7024
7302
|
fs: fs5,
|
|
7025
7303
|
logger: {
|
|
@@ -7040,7 +7318,7 @@ async function runLayerChecks(fs5, _ctx) {
|
|
|
7040
7318
|
}, {});
|
|
7041
7319
|
results.push(checkHasFeatures(discovery.stats.features));
|
|
7042
7320
|
results.push(checkHasExamples(discovery.stats.examples));
|
|
7043
|
-
results.push(checkFeatureOwners(discovery.inventory.features));
|
|
7321
|
+
results.push(checkFeatureOwners(discovery.inventory.features, policy, ctx));
|
|
7044
7322
|
results.push(checkExampleEntrypoints(discovery.inventory.examples));
|
|
7045
7323
|
results.push(checkWorkspaceConfigs(discovery.inventory.workspaceConfigs));
|
|
7046
7324
|
return results;
|
|
@@ -7079,27 +7357,43 @@ function checkHasExamples(count) {
|
|
|
7079
7357
|
details: "Create an example.ts file to package reusable templates"
|
|
7080
7358
|
};
|
|
7081
7359
|
}
|
|
7082
|
-
function checkFeatureOwners(features) {
|
|
7360
|
+
function checkFeatureOwners(features, policy, ctx) {
|
|
7083
7361
|
const missingOwners = [];
|
|
7362
|
+
const criticalMissingOwners = [];
|
|
7084
7363
|
for (const [key, feature] of features) {
|
|
7085
|
-
|
|
7086
|
-
|
|
7364
|
+
const hasOwnerMetadata = Boolean(feature.owners?.length) || /owners\s*:\s*(?!\[\s*\])/.test(feature.sourceBlock ?? "");
|
|
7365
|
+
if (!hasOwnerMetadata) {
|
|
7366
|
+
if (isCriticalFeatureKey(key, policy)) {
|
|
7367
|
+
criticalMissingOwners.push(key);
|
|
7368
|
+
} else {
|
|
7369
|
+
missingOwners.push(key);
|
|
7370
|
+
}
|
|
7087
7371
|
}
|
|
7088
7372
|
}
|
|
7089
|
-
if (missingOwners.length === 0) {
|
|
7373
|
+
if (criticalMissingOwners.length === 0 && missingOwners.length === 0) {
|
|
7090
7374
|
return {
|
|
7091
7375
|
category: "layers",
|
|
7092
7376
|
name: "Feature Owners",
|
|
7093
7377
|
status: features.size > 0 ? "pass" : "skip",
|
|
7094
|
-
message: features.size > 0 ? "All features have owners defined" : "No features to check"
|
|
7378
|
+
message: features.size > 0 ? "All features have owners defined" : "No features to check",
|
|
7379
|
+
context: features.size > 0 ? {
|
|
7380
|
+
policyPath: policy ? getStabilityPolicyPath(ctx.workspaceRoot) : undefined,
|
|
7381
|
+
criticalMissingFeatures: [],
|
|
7382
|
+
missingFeatures: []
|
|
7383
|
+
} : undefined
|
|
7095
7384
|
};
|
|
7096
7385
|
}
|
|
7097
7386
|
return {
|
|
7098
7387
|
category: "layers",
|
|
7099
7388
|
name: "Feature Owners",
|
|
7100
|
-
status: "warn",
|
|
7101
|
-
message: `${missingOwners.length} feature(s) missing owners`,
|
|
7102
|
-
details: `Features: ${missingOwners.slice(0, 3).join(", ")}${missingOwners.length > 3 ? "..." : ""}
|
|
7389
|
+
status: criticalMissingOwners.length > 0 ? "fail" : "warn",
|
|
7390
|
+
message: criticalMissingOwners.length > 0 ? `${criticalMissingOwners.length} critical feature(s) missing owners` : `${missingOwners.length} feature(s) missing owners`,
|
|
7391
|
+
details: criticalMissingOwners.length > 0 ? `Critical features: ${criticalMissingOwners.join(", ")}` : `Features: ${missingOwners.slice(0, 3).join(", ")}${missingOwners.length > 3 ? "..." : ""}`,
|
|
7392
|
+
context: {
|
|
7393
|
+
policyPath: policy ? getStabilityPolicyPath(ctx.workspaceRoot) : undefined,
|
|
7394
|
+
criticalMissingFeatures: criticalMissingOwners,
|
|
7395
|
+
missingFeatures: missingOwners
|
|
7396
|
+
}
|
|
7103
7397
|
};
|
|
7104
7398
|
}
|
|
7105
7399
|
function checkExampleEntrypoints(examples) {
|
|
@@ -7636,7 +7930,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7636
7930
|
const result = await runDoctor(adapters, {
|
|
7637
7931
|
workspaceRoot,
|
|
7638
7932
|
skipAi: true,
|
|
7639
|
-
categories: ["cli", "config", "deps", "workspace"]
|
|
7933
|
+
categories: ["cli", "config", "deps", "workspace", "layers"]
|
|
7640
7934
|
});
|
|
7641
7935
|
for (const check of result.checks) {
|
|
7642
7936
|
if (check.status === "fail") {
|
|
@@ -7645,7 +7939,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7645
7939
|
severity: "error",
|
|
7646
7940
|
message: `${check.name}: ${check.message}`,
|
|
7647
7941
|
category: "doctor",
|
|
7648
|
-
context: { details: check.details }
|
|
7942
|
+
context: { details: check.details, ...check.context ?? {} }
|
|
7649
7943
|
});
|
|
7650
7944
|
} else if (check.status === "warn") {
|
|
7651
7945
|
issues.push({
|
|
@@ -7653,7 +7947,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7653
7947
|
severity: "warning",
|
|
7654
7948
|
message: `${check.name}: ${check.message}`,
|
|
7655
7949
|
category: "doctor",
|
|
7656
|
-
context: { details: check.details }
|
|
7950
|
+
context: { details: check.details, ...check.context ?? {} }
|
|
7657
7951
|
});
|
|
7658
7952
|
}
|
|
7659
7953
|
}
|
|
@@ -16851,20 +17145,36 @@ function formatAsJson(result, options = {}) {
|
|
|
16851
17145
|
line: issue.line,
|
|
16852
17146
|
details: issue.context
|
|
16853
17147
|
}));
|
|
17148
|
+
const pass = result.categories.filter((category) => category.passed).length;
|
|
16854
17149
|
const fail = result.totalErrors;
|
|
16855
17150
|
const warn = result.totalWarnings;
|
|
17151
|
+
const note = result.totalNotes;
|
|
16856
17152
|
const output = {
|
|
16857
17153
|
schemaVersion: "1.0",
|
|
17154
|
+
success: result.success,
|
|
16858
17155
|
checks,
|
|
17156
|
+
categories: result.categories.map((category) => ({
|
|
17157
|
+
category: category.category,
|
|
17158
|
+
label: category.label,
|
|
17159
|
+
passed: category.passed,
|
|
17160
|
+
errors: category.errors,
|
|
17161
|
+
warnings: category.warnings,
|
|
17162
|
+
notes: category.notes,
|
|
17163
|
+
durationMs: category.durationMs
|
|
17164
|
+
})),
|
|
16859
17165
|
drift: {
|
|
16860
17166
|
status: driftResult?.hasDrift ? "detected" : "none",
|
|
16861
17167
|
files: driftResult?.files ?? []
|
|
16862
17168
|
},
|
|
16863
17169
|
summary: {
|
|
16864
|
-
pass
|
|
17170
|
+
pass,
|
|
16865
17171
|
fail,
|
|
16866
17172
|
warn,
|
|
16867
|
-
|
|
17173
|
+
note,
|
|
17174
|
+
total: pass + fail + warn + note,
|
|
17175
|
+
totalErrors: result.totalErrors,
|
|
17176
|
+
totalWarnings: result.totalWarnings,
|
|
17177
|
+
totalNotes: result.totalNotes,
|
|
16868
17178
|
durationMs: result.durationMs,
|
|
16869
17179
|
timestamp: result.timestamp
|
|
16870
17180
|
},
|
|
@@ -8,4 +8,4 @@ import type { CheckContext, CheckResult } from '../types';
|
|
|
8
8
|
/**
|
|
9
9
|
* Run all layer health checks.
|
|
10
10
|
*/
|
|
11
|
-
export declare function runLayerChecks(fs: FsAdapter,
|
|
11
|
+
export declare function runLayerChecks(fs: FsAdapter, ctx: CheckContext): Promise<CheckResult[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -53,6 +53,8 @@ export interface CheckResult {
|
|
|
53
53
|
fix?: FixAction;
|
|
54
54
|
/** Additional details for debugging. */
|
|
55
55
|
details?: string;
|
|
56
|
+
/** Structured context for machine-readable consumers. */
|
|
57
|
+
context?: Record<string, unknown>;
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Options for running the doctor.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FsAdapter } from '../../ports/fs';
|
|
2
|
+
import type { StabilityPolicy, StabilityTier } from './policy';
|
|
3
|
+
export type PackageAuditFindingCode = 'critical-missing-build-script' | 'critical-missing-typecheck-script' | 'critical-missing-lint-script' | 'critical-missing-test-script' | 'critical-missing-test-files' | 'critical-pass-with-no-tests' | 'tests-without-test-script' | 'build-without-tests';
|
|
4
|
+
export interface PackageAuditFinding {
|
|
5
|
+
code: PackageAuditFindingCode;
|
|
6
|
+
tier: StabilityTier;
|
|
7
|
+
packageName: string;
|
|
8
|
+
packagePath: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CriticalPackageStatus {
|
|
12
|
+
packageName: string;
|
|
13
|
+
packagePath: string;
|
|
14
|
+
hasBuildScript: boolean;
|
|
15
|
+
hasTypecheckScript: boolean;
|
|
16
|
+
hasLintScript: boolean;
|
|
17
|
+
hasTestScript: boolean;
|
|
18
|
+
usesPassWithNoTests: boolean;
|
|
19
|
+
testFileCount: number;
|
|
20
|
+
}
|
|
21
|
+
export interface PackageAuditReport {
|
|
22
|
+
findings: PackageAuditFinding[];
|
|
23
|
+
criticalPackages: CriticalPackageStatus[];
|
|
24
|
+
}
|
|
25
|
+
export declare function auditWorkspacePackages(fs: FsAdapter, workspaceRoot: string, policy: StabilityPolicy): Promise<PackageAuditReport>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FsAdapter } from '../../ports/fs';
|
|
2
|
+
export type StabilityTier = 'critical' | 'non-critical';
|
|
3
|
+
export interface StabilityPolicy {
|
|
4
|
+
version: number;
|
|
5
|
+
criticalPackages: string[];
|
|
6
|
+
criticalFeatureKeys: string[];
|
|
7
|
+
smokePackages: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function loadStabilityPolicy(fs: FsAdapter, workspaceRoot: string): Promise<StabilityPolicy | undefined>;
|
|
10
|
+
export declare function getPackageTier(relativePackagePath: string, policy?: StabilityPolicy): StabilityTier;
|
|
11
|
+
export declare function isCriticalFeatureKey(featureKey: string, policy?: StabilityPolicy): boolean;
|
|
12
|
+
export declare function getStabilityPolicyPath(workspaceRoot: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/bundle.workspace",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.4",
|
|
4
4
|
"description": "Workspace utilities for monorepo development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@ai-sdk/anthropic": "3.0.58",
|
|
35
35
|
"@ai-sdk/openai": "3.0.41",
|
|
36
|
-
"@contractspec/lib.ai-agent": "7.0.
|
|
37
|
-
"@contractspec/lib.ai-providers": "3.7.
|
|
38
|
-
"@contractspec/lib.contracts-spec": "3.7.
|
|
39
|
-
"@contractspec/lib.contracts-integrations": "3.7.
|
|
40
|
-
"@contractspec/lib.contracts-transformers": "3.7.
|
|
41
|
-
"@contractspec/lib.source-extractors": "2.7.
|
|
42
|
-
"@contractspec/module.workspace": "3.7.
|
|
43
|
-
"@contractspec/lib.utils-typescript": "3.7.
|
|
36
|
+
"@contractspec/lib.ai-agent": "7.0.4",
|
|
37
|
+
"@contractspec/lib.ai-providers": "3.7.4",
|
|
38
|
+
"@contractspec/lib.contracts-spec": "3.7.4",
|
|
39
|
+
"@contractspec/lib.contracts-integrations": "3.7.4",
|
|
40
|
+
"@contractspec/lib.contracts-transformers": "3.7.4",
|
|
41
|
+
"@contractspec/lib.source-extractors": "2.7.4",
|
|
42
|
+
"@contractspec/module.workspace": "3.7.4",
|
|
43
|
+
"@contractspec/lib.utils-typescript": "3.7.4",
|
|
44
44
|
"ai": "6.0.116",
|
|
45
45
|
"chalk": "^5.6.2",
|
|
46
46
|
"chokidar": "^5.0.0",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"zod": "^4.3.5"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@contractspec/tool.typescript": "3.7.
|
|
55
|
+
"@contractspec/tool.typescript": "3.7.4",
|
|
56
56
|
"@types/bun": "^1.3.10",
|
|
57
57
|
"@types/micromatch": "^4.0.10",
|
|
58
58
|
"@types/node": "^25.3.5",
|
|
59
59
|
"typescript": "^5.9.3",
|
|
60
|
-
"@contractspec/tool.bun": "3.7.
|
|
60
|
+
"@contractspec/tool.bun": "3.7.4"
|
|
61
61
|
},
|
|
62
62
|
"exports": {
|
|
63
63
|
".": {
|