@contractspec/bundle.workspace 3.7.0 → 3.7.3
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 +353 -30
- package/dist/node/index.js +353 -30
- 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",
|
|
@@ -759,7 +769,8 @@ function createNodeAdapters(options = {}) {
|
|
|
759
769
|
}
|
|
760
770
|
// src/adapters/workspace.ts
|
|
761
771
|
import { existsSync, readFileSync } from "fs";
|
|
762
|
-
import {
|
|
772
|
+
import { tmpdir } from "os";
|
|
773
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative3, resolve as resolve4 } from "path";
|
|
763
774
|
var LOCK_FILES = {
|
|
764
775
|
"bun.lockb": "bun",
|
|
765
776
|
"bun.lock": "bun",
|
|
@@ -774,14 +785,24 @@ var MONOREPO_FILES = [
|
|
|
774
785
|
"turbo.json",
|
|
775
786
|
"rush.json"
|
|
776
787
|
];
|
|
788
|
+
function isWithinDirectory(target, directory) {
|
|
789
|
+
const pathFromDirectory = relative3(directory, target);
|
|
790
|
+
return pathFromDirectory === "" || !pathFromDirectory.startsWith("..") && !isAbsolute3(pathFromDirectory);
|
|
791
|
+
}
|
|
792
|
+
function getTraversalBoundary(startDir) {
|
|
793
|
+
const resolvedStartDir = resolve4(startDir);
|
|
794
|
+
const tempRoot = resolve4(tmpdir());
|
|
795
|
+
return isWithinDirectory(resolvedStartDir, tempRoot) ? tempRoot : undefined;
|
|
796
|
+
}
|
|
777
797
|
function findPackageRoot(startDir = process.cwd()) {
|
|
778
798
|
let current = resolve4(startDir);
|
|
799
|
+
const traversalBoundary = getTraversalBoundary(startDir);
|
|
779
800
|
while (true) {
|
|
780
801
|
if (existsSync(join3(current, "package.json"))) {
|
|
781
802
|
return current;
|
|
782
803
|
}
|
|
783
804
|
const parent = dirname3(current);
|
|
784
|
-
if (parent === current) {
|
|
805
|
+
if (parent === current || current === traversalBoundary) {
|
|
785
806
|
break;
|
|
786
807
|
}
|
|
787
808
|
current = parent;
|
|
@@ -791,6 +812,7 @@ function findPackageRoot(startDir = process.cwd()) {
|
|
|
791
812
|
function findWorkspaceRoot(startDir = process.cwd()) {
|
|
792
813
|
let current = resolve4(startDir);
|
|
793
814
|
let lastPackageJson = null;
|
|
815
|
+
const traversalBoundary = getTraversalBoundary(startDir);
|
|
794
816
|
while (true) {
|
|
795
817
|
for (const file of MONOREPO_FILES) {
|
|
796
818
|
if (existsSync(join3(current, file))) {
|
|
@@ -813,7 +835,7 @@ function findWorkspaceRoot(startDir = process.cwd()) {
|
|
|
813
835
|
}
|
|
814
836
|
}
|
|
815
837
|
const parent = dirname3(current);
|
|
816
|
-
if (parent === current) {
|
|
838
|
+
if (parent === current || current === traversalBoundary) {
|
|
817
839
|
break;
|
|
818
840
|
}
|
|
819
841
|
current = parent;
|
|
@@ -942,12 +964,13 @@ function checkHasWorkspaces(dir) {
|
|
|
942
964
|
}
|
|
943
965
|
function findMetaRepoRoot(startDir) {
|
|
944
966
|
let current = resolve4(startDir);
|
|
967
|
+
const traversalBoundary = getTraversalBoundary(startDir);
|
|
945
968
|
while (true) {
|
|
946
969
|
if (existsSync(join3(current, ".gitmodules"))) {
|
|
947
970
|
return current;
|
|
948
971
|
}
|
|
949
972
|
const parent = dirname3(current);
|
|
950
|
-
if (parent === current) {
|
|
973
|
+
if (parent === current || current === traversalBoundary) {
|
|
951
974
|
break;
|
|
952
975
|
}
|
|
953
976
|
current = parent;
|
|
@@ -1815,7 +1838,7 @@ ${task.existingCode}`;
|
|
|
1815
1838
|
import { spawn } from "child_process";
|
|
1816
1839
|
import { mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1817
1840
|
import { join as join4 } from "path";
|
|
1818
|
-
import { homedir, tmpdir } from "os";
|
|
1841
|
+
import { homedir, tmpdir as tmpdir2 } from "os";
|
|
1819
1842
|
import { existsSync as existsSync2 } from "fs";
|
|
1820
1843
|
|
|
1821
1844
|
class CursorAgent {
|
|
@@ -1832,7 +1855,7 @@ class CursorAgent {
|
|
|
1832
1855
|
}
|
|
1833
1856
|
async generate(task) {
|
|
1834
1857
|
try {
|
|
1835
|
-
const workDir = join4(
|
|
1858
|
+
const workDir = join4(tmpdir2(), `cursor-agent-${Date.now()}`);
|
|
1836
1859
|
await mkdir3(workDir, { recursive: true });
|
|
1837
1860
|
const result = await this.executeWithBestMethod(task, workDir);
|
|
1838
1861
|
await this.cleanupWorkDir(workDir);
|
|
@@ -1846,7 +1869,7 @@ class CursorAgent {
|
|
|
1846
1869
|
}
|
|
1847
1870
|
async validate(task) {
|
|
1848
1871
|
try {
|
|
1849
|
-
const workDir = join4(
|
|
1872
|
+
const workDir = join4(tmpdir2(), `cursor-validate-${Date.now()}`);
|
|
1850
1873
|
await mkdir3(workDir, { recursive: true });
|
|
1851
1874
|
await this.setupValidationWorkspace(task, workDir);
|
|
1852
1875
|
const result = await this.executeWithBestMethod({
|
|
@@ -3075,9 +3098,7 @@ async function analyzeDeps(adapters, options = {}) {
|
|
|
3075
3098
|
for (const file of files) {
|
|
3076
3099
|
const content = await fs5.readFile(file);
|
|
3077
3100
|
const relativePath = fs5.relative(".", file);
|
|
3078
|
-
const
|
|
3079
|
-
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)$/, "");
|
|
3080
|
-
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";
|
|
3081
3102
|
const dependencies = parseImportedSpecNames(content, file);
|
|
3082
3103
|
addContractNode(graph, finalName, relativePath, dependencies);
|
|
3083
3104
|
}
|
|
@@ -6657,6 +6678,271 @@ async function checkContractsLibrary(fs5, ctx) {
|
|
|
6657
6678
|
};
|
|
6658
6679
|
}
|
|
6659
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
|
+
|
|
6660
6946
|
// src/services/doctor/checks/workspace.ts
|
|
6661
6947
|
var CONTRACT_PATHS = ["src/contracts", "contracts", "src/specs", "specs"];
|
|
6662
6948
|
async function runWorkspaceChecks(fs5, ctx) {
|
|
@@ -6666,6 +6952,7 @@ async function runWorkspaceChecks(fs5, ctx) {
|
|
|
6666
6952
|
results.push(await checkContractsDirectory(fs5, ctx));
|
|
6667
6953
|
results.push(await checkContractFiles(fs5, ctx));
|
|
6668
6954
|
results.push(await checkOutputDirectory(fs5, ctx));
|
|
6955
|
+
results.push(...await runPackageStabilityChecks(fs5, ctx));
|
|
6669
6956
|
return results;
|
|
6670
6957
|
}
|
|
6671
6958
|
function checkMonorepoStatus(ctx) {
|
|
@@ -6847,12 +7134,15 @@ async function checkOutputDirectory(fs5, ctx) {
|
|
|
6847
7134
|
details: ctx.verbose ? `Resolved to: ${outputPath}` : undefined
|
|
6848
7135
|
};
|
|
6849
7136
|
}
|
|
6850
|
-
|
|
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")) {
|
|
6851
7140
|
return {
|
|
6852
7141
|
category: "workspace",
|
|
6853
7142
|
name: "Output Directory",
|
|
6854
7143
|
status: "pass",
|
|
6855
|
-
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
|
|
6856
7146
|
};
|
|
6857
7147
|
}
|
|
6858
7148
|
return {
|
|
@@ -7005,8 +7295,9 @@ async function checkApiKey(fs5, ctx, prompts) {
|
|
|
7005
7295
|
}
|
|
7006
7296
|
}
|
|
7007
7297
|
// src/services/doctor/checks/layers.ts
|
|
7008
|
-
async function runLayerChecks(fs5,
|
|
7298
|
+
async function runLayerChecks(fs5, ctx) {
|
|
7009
7299
|
const results = [];
|
|
7300
|
+
const policy = await loadStabilityPolicy(fs5, ctx.workspaceRoot);
|
|
7010
7301
|
const discovery = await discoverLayers({
|
|
7011
7302
|
fs: fs5,
|
|
7012
7303
|
logger: {
|
|
@@ -7027,7 +7318,7 @@ async function runLayerChecks(fs5, _ctx) {
|
|
|
7027
7318
|
}, {});
|
|
7028
7319
|
results.push(checkHasFeatures(discovery.stats.features));
|
|
7029
7320
|
results.push(checkHasExamples(discovery.stats.examples));
|
|
7030
|
-
results.push(checkFeatureOwners(discovery.inventory.features));
|
|
7321
|
+
results.push(checkFeatureOwners(discovery.inventory.features, policy, ctx));
|
|
7031
7322
|
results.push(checkExampleEntrypoints(discovery.inventory.examples));
|
|
7032
7323
|
results.push(checkWorkspaceConfigs(discovery.inventory.workspaceConfigs));
|
|
7033
7324
|
return results;
|
|
@@ -7066,27 +7357,43 @@ function checkHasExamples(count) {
|
|
|
7066
7357
|
details: "Create an example.ts file to package reusable templates"
|
|
7067
7358
|
};
|
|
7068
7359
|
}
|
|
7069
|
-
function checkFeatureOwners(features) {
|
|
7360
|
+
function checkFeatureOwners(features, policy, ctx) {
|
|
7070
7361
|
const missingOwners = [];
|
|
7362
|
+
const criticalMissingOwners = [];
|
|
7071
7363
|
for (const [key, feature] of features) {
|
|
7072
|
-
|
|
7073
|
-
|
|
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
|
+
}
|
|
7074
7371
|
}
|
|
7075
7372
|
}
|
|
7076
|
-
if (missingOwners.length === 0) {
|
|
7373
|
+
if (criticalMissingOwners.length === 0 && missingOwners.length === 0) {
|
|
7077
7374
|
return {
|
|
7078
7375
|
category: "layers",
|
|
7079
7376
|
name: "Feature Owners",
|
|
7080
7377
|
status: features.size > 0 ? "pass" : "skip",
|
|
7081
|
-
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
|
|
7082
7384
|
};
|
|
7083
7385
|
}
|
|
7084
7386
|
return {
|
|
7085
7387
|
category: "layers",
|
|
7086
7388
|
name: "Feature Owners",
|
|
7087
|
-
status: "warn",
|
|
7088
|
-
message: `${missingOwners.length} feature(s) missing owners`,
|
|
7089
|
-
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
|
+
}
|
|
7090
7397
|
};
|
|
7091
7398
|
}
|
|
7092
7399
|
function checkExampleEntrypoints(examples) {
|
|
@@ -7623,7 +7930,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7623
7930
|
const result = await runDoctor(adapters, {
|
|
7624
7931
|
workspaceRoot,
|
|
7625
7932
|
skipAi: true,
|
|
7626
|
-
categories: ["cli", "config", "deps", "workspace"]
|
|
7933
|
+
categories: ["cli", "config", "deps", "workspace", "layers"]
|
|
7627
7934
|
});
|
|
7628
7935
|
for (const check of result.checks) {
|
|
7629
7936
|
if (check.status === "fail") {
|
|
@@ -7632,7 +7939,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7632
7939
|
severity: "error",
|
|
7633
7940
|
message: `${check.name}: ${check.message}`,
|
|
7634
7941
|
category: "doctor",
|
|
7635
|
-
context: { details: check.details }
|
|
7942
|
+
context: { details: check.details, ...check.context ?? {} }
|
|
7636
7943
|
});
|
|
7637
7944
|
} else if (check.status === "warn") {
|
|
7638
7945
|
issues.push({
|
|
@@ -7640,7 +7947,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7640
7947
|
severity: "warning",
|
|
7641
7948
|
message: `${check.name}: ${check.message}`,
|
|
7642
7949
|
category: "doctor",
|
|
7643
|
-
context: { details: check.details }
|
|
7950
|
+
context: { details: check.details, ...check.context ?? {} }
|
|
7644
7951
|
});
|
|
7645
7952
|
}
|
|
7646
7953
|
}
|
|
@@ -8577,7 +8884,7 @@ init_config();
|
|
|
8577
8884
|
// src/services/drift.ts
|
|
8578
8885
|
import path4 from "path";
|
|
8579
8886
|
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
8580
|
-
import { tmpdir as
|
|
8887
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
8581
8888
|
|
|
8582
8889
|
// src/services/generate-artifacts.ts
|
|
8583
8890
|
import path3 from "path";
|
|
@@ -8737,7 +9044,7 @@ async function generateArtifacts(adapters, contractsDir, generatedDir, rootPath)
|
|
|
8737
9044
|
|
|
8738
9045
|
// src/services/drift.ts
|
|
8739
9046
|
async function detectDrift(adapters, contractsDir, generatedDir) {
|
|
8740
|
-
const tempDir = await mkdtemp(path4.join(
|
|
9047
|
+
const tempDir = await mkdtemp(path4.join(tmpdir3(), "contractspec-drift-"));
|
|
8741
9048
|
try {
|
|
8742
9049
|
await generateArtifacts(adapters, contractsDir, tempDir);
|
|
8743
9050
|
const differences = [];
|
|
@@ -16838,20 +17145,36 @@ function formatAsJson(result, options = {}) {
|
|
|
16838
17145
|
line: issue.line,
|
|
16839
17146
|
details: issue.context
|
|
16840
17147
|
}));
|
|
17148
|
+
const pass = result.categories.filter((category) => category.passed).length;
|
|
16841
17149
|
const fail = result.totalErrors;
|
|
16842
17150
|
const warn = result.totalWarnings;
|
|
17151
|
+
const note = result.totalNotes;
|
|
16843
17152
|
const output = {
|
|
16844
17153
|
schemaVersion: "1.0",
|
|
17154
|
+
success: result.success,
|
|
16845
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
|
+
})),
|
|
16846
17165
|
drift: {
|
|
16847
17166
|
status: driftResult?.hasDrift ? "detected" : "none",
|
|
16848
17167
|
files: driftResult?.files ?? []
|
|
16849
17168
|
},
|
|
16850
17169
|
summary: {
|
|
16851
|
-
pass
|
|
17170
|
+
pass,
|
|
16852
17171
|
fail,
|
|
16853
17172
|
warn,
|
|
16854
|
-
|
|
17173
|
+
note,
|
|
17174
|
+
total: pass + fail + warn + note,
|
|
17175
|
+
totalErrors: result.totalErrors,
|
|
17176
|
+
totalWarnings: result.totalWarnings,
|
|
17177
|
+
totalNotes: result.totalNotes,
|
|
16855
17178
|
durationMs: result.durationMs,
|
|
16856
17179
|
timestamp: result.timestamp
|
|
16857
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",
|
|
@@ -759,7 +769,8 @@ function createNodeAdapters(options = {}) {
|
|
|
759
769
|
}
|
|
760
770
|
// src/adapters/workspace.ts
|
|
761
771
|
import { existsSync, readFileSync } from "node:fs";
|
|
762
|
-
import {
|
|
772
|
+
import { tmpdir } from "node:os";
|
|
773
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative3, resolve as resolve4 } from "node:path";
|
|
763
774
|
var LOCK_FILES = {
|
|
764
775
|
"bun.lockb": "bun",
|
|
765
776
|
"bun.lock": "bun",
|
|
@@ -774,14 +785,24 @@ var MONOREPO_FILES = [
|
|
|
774
785
|
"turbo.json",
|
|
775
786
|
"rush.json"
|
|
776
787
|
];
|
|
788
|
+
function isWithinDirectory(target, directory) {
|
|
789
|
+
const pathFromDirectory = relative3(directory, target);
|
|
790
|
+
return pathFromDirectory === "" || !pathFromDirectory.startsWith("..") && !isAbsolute3(pathFromDirectory);
|
|
791
|
+
}
|
|
792
|
+
function getTraversalBoundary(startDir) {
|
|
793
|
+
const resolvedStartDir = resolve4(startDir);
|
|
794
|
+
const tempRoot = resolve4(tmpdir());
|
|
795
|
+
return isWithinDirectory(resolvedStartDir, tempRoot) ? tempRoot : undefined;
|
|
796
|
+
}
|
|
777
797
|
function findPackageRoot(startDir = process.cwd()) {
|
|
778
798
|
let current = resolve4(startDir);
|
|
799
|
+
const traversalBoundary = getTraversalBoundary(startDir);
|
|
779
800
|
while (true) {
|
|
780
801
|
if (existsSync(join3(current, "package.json"))) {
|
|
781
802
|
return current;
|
|
782
803
|
}
|
|
783
804
|
const parent = dirname3(current);
|
|
784
|
-
if (parent === current) {
|
|
805
|
+
if (parent === current || current === traversalBoundary) {
|
|
785
806
|
break;
|
|
786
807
|
}
|
|
787
808
|
current = parent;
|
|
@@ -791,6 +812,7 @@ function findPackageRoot(startDir = process.cwd()) {
|
|
|
791
812
|
function findWorkspaceRoot(startDir = process.cwd()) {
|
|
792
813
|
let current = resolve4(startDir);
|
|
793
814
|
let lastPackageJson = null;
|
|
815
|
+
const traversalBoundary = getTraversalBoundary(startDir);
|
|
794
816
|
while (true) {
|
|
795
817
|
for (const file of MONOREPO_FILES) {
|
|
796
818
|
if (existsSync(join3(current, file))) {
|
|
@@ -813,7 +835,7 @@ function findWorkspaceRoot(startDir = process.cwd()) {
|
|
|
813
835
|
}
|
|
814
836
|
}
|
|
815
837
|
const parent = dirname3(current);
|
|
816
|
-
if (parent === current) {
|
|
838
|
+
if (parent === current || current === traversalBoundary) {
|
|
817
839
|
break;
|
|
818
840
|
}
|
|
819
841
|
current = parent;
|
|
@@ -942,12 +964,13 @@ function checkHasWorkspaces(dir) {
|
|
|
942
964
|
}
|
|
943
965
|
function findMetaRepoRoot(startDir) {
|
|
944
966
|
let current = resolve4(startDir);
|
|
967
|
+
const traversalBoundary = getTraversalBoundary(startDir);
|
|
945
968
|
while (true) {
|
|
946
969
|
if (existsSync(join3(current, ".gitmodules"))) {
|
|
947
970
|
return current;
|
|
948
971
|
}
|
|
949
972
|
const parent = dirname3(current);
|
|
950
|
-
if (parent === current) {
|
|
973
|
+
if (parent === current || current === traversalBoundary) {
|
|
951
974
|
break;
|
|
952
975
|
}
|
|
953
976
|
current = parent;
|
|
@@ -1815,7 +1838,7 @@ ${task.existingCode}`;
|
|
|
1815
1838
|
import { spawn } from "child_process";
|
|
1816
1839
|
import { mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1817
1840
|
import { join as join4 } from "path";
|
|
1818
|
-
import { homedir, tmpdir } from "os";
|
|
1841
|
+
import { homedir, tmpdir as tmpdir2 } from "os";
|
|
1819
1842
|
import { existsSync as existsSync2 } from "fs";
|
|
1820
1843
|
|
|
1821
1844
|
class CursorAgent {
|
|
@@ -1832,7 +1855,7 @@ class CursorAgent {
|
|
|
1832
1855
|
}
|
|
1833
1856
|
async generate(task) {
|
|
1834
1857
|
try {
|
|
1835
|
-
const workDir = join4(
|
|
1858
|
+
const workDir = join4(tmpdir2(), `cursor-agent-${Date.now()}`);
|
|
1836
1859
|
await mkdir3(workDir, { recursive: true });
|
|
1837
1860
|
const result = await this.executeWithBestMethod(task, workDir);
|
|
1838
1861
|
await this.cleanupWorkDir(workDir);
|
|
@@ -1846,7 +1869,7 @@ class CursorAgent {
|
|
|
1846
1869
|
}
|
|
1847
1870
|
async validate(task) {
|
|
1848
1871
|
try {
|
|
1849
|
-
const workDir = join4(
|
|
1872
|
+
const workDir = join4(tmpdir2(), `cursor-validate-${Date.now()}`);
|
|
1850
1873
|
await mkdir3(workDir, { recursive: true });
|
|
1851
1874
|
await this.setupValidationWorkspace(task, workDir);
|
|
1852
1875
|
const result = await this.executeWithBestMethod({
|
|
@@ -3075,9 +3098,7 @@ async function analyzeDeps(adapters, options = {}) {
|
|
|
3075
3098
|
for (const file of files) {
|
|
3076
3099
|
const content = await fs5.readFile(file);
|
|
3077
3100
|
const relativePath = fs5.relative(".", file);
|
|
3078
|
-
const
|
|
3079
|
-
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)$/, "");
|
|
3080
|
-
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";
|
|
3081
3102
|
const dependencies = parseImportedSpecNames(content, file);
|
|
3082
3103
|
addContractNode(graph, finalName, relativePath, dependencies);
|
|
3083
3104
|
}
|
|
@@ -6657,6 +6678,271 @@ async function checkContractsLibrary(fs5, ctx) {
|
|
|
6657
6678
|
};
|
|
6658
6679
|
}
|
|
6659
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
|
+
|
|
6660
6946
|
// src/services/doctor/checks/workspace.ts
|
|
6661
6947
|
var CONTRACT_PATHS = ["src/contracts", "contracts", "src/specs", "specs"];
|
|
6662
6948
|
async function runWorkspaceChecks(fs5, ctx) {
|
|
@@ -6666,6 +6952,7 @@ async function runWorkspaceChecks(fs5, ctx) {
|
|
|
6666
6952
|
results.push(await checkContractsDirectory(fs5, ctx));
|
|
6667
6953
|
results.push(await checkContractFiles(fs5, ctx));
|
|
6668
6954
|
results.push(await checkOutputDirectory(fs5, ctx));
|
|
6955
|
+
results.push(...await runPackageStabilityChecks(fs5, ctx));
|
|
6669
6956
|
return results;
|
|
6670
6957
|
}
|
|
6671
6958
|
function checkMonorepoStatus(ctx) {
|
|
@@ -6847,12 +7134,15 @@ async function checkOutputDirectory(fs5, ctx) {
|
|
|
6847
7134
|
details: ctx.verbose ? `Resolved to: ${outputPath}` : undefined
|
|
6848
7135
|
};
|
|
6849
7136
|
}
|
|
6850
|
-
|
|
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")) {
|
|
6851
7140
|
return {
|
|
6852
7141
|
category: "workspace",
|
|
6853
7142
|
name: "Output Directory",
|
|
6854
7143
|
status: "pass",
|
|
6855
|
-
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
|
|
6856
7146
|
};
|
|
6857
7147
|
}
|
|
6858
7148
|
return {
|
|
@@ -7005,8 +7295,9 @@ async function checkApiKey(fs5, ctx, prompts) {
|
|
|
7005
7295
|
}
|
|
7006
7296
|
}
|
|
7007
7297
|
// src/services/doctor/checks/layers.ts
|
|
7008
|
-
async function runLayerChecks(fs5,
|
|
7298
|
+
async function runLayerChecks(fs5, ctx) {
|
|
7009
7299
|
const results = [];
|
|
7300
|
+
const policy = await loadStabilityPolicy(fs5, ctx.workspaceRoot);
|
|
7010
7301
|
const discovery = await discoverLayers({
|
|
7011
7302
|
fs: fs5,
|
|
7012
7303
|
logger: {
|
|
@@ -7027,7 +7318,7 @@ async function runLayerChecks(fs5, _ctx) {
|
|
|
7027
7318
|
}, {});
|
|
7028
7319
|
results.push(checkHasFeatures(discovery.stats.features));
|
|
7029
7320
|
results.push(checkHasExamples(discovery.stats.examples));
|
|
7030
|
-
results.push(checkFeatureOwners(discovery.inventory.features));
|
|
7321
|
+
results.push(checkFeatureOwners(discovery.inventory.features, policy, ctx));
|
|
7031
7322
|
results.push(checkExampleEntrypoints(discovery.inventory.examples));
|
|
7032
7323
|
results.push(checkWorkspaceConfigs(discovery.inventory.workspaceConfigs));
|
|
7033
7324
|
return results;
|
|
@@ -7066,27 +7357,43 @@ function checkHasExamples(count) {
|
|
|
7066
7357
|
details: "Create an example.ts file to package reusable templates"
|
|
7067
7358
|
};
|
|
7068
7359
|
}
|
|
7069
|
-
function checkFeatureOwners(features) {
|
|
7360
|
+
function checkFeatureOwners(features, policy, ctx) {
|
|
7070
7361
|
const missingOwners = [];
|
|
7362
|
+
const criticalMissingOwners = [];
|
|
7071
7363
|
for (const [key, feature] of features) {
|
|
7072
|
-
|
|
7073
|
-
|
|
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
|
+
}
|
|
7074
7371
|
}
|
|
7075
7372
|
}
|
|
7076
|
-
if (missingOwners.length === 0) {
|
|
7373
|
+
if (criticalMissingOwners.length === 0 && missingOwners.length === 0) {
|
|
7077
7374
|
return {
|
|
7078
7375
|
category: "layers",
|
|
7079
7376
|
name: "Feature Owners",
|
|
7080
7377
|
status: features.size > 0 ? "pass" : "skip",
|
|
7081
|
-
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
|
|
7082
7384
|
};
|
|
7083
7385
|
}
|
|
7084
7386
|
return {
|
|
7085
7387
|
category: "layers",
|
|
7086
7388
|
name: "Feature Owners",
|
|
7087
|
-
status: "warn",
|
|
7088
|
-
message: `${missingOwners.length} feature(s) missing owners`,
|
|
7089
|
-
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
|
+
}
|
|
7090
7397
|
};
|
|
7091
7398
|
}
|
|
7092
7399
|
function checkExampleEntrypoints(examples) {
|
|
@@ -7623,7 +7930,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7623
7930
|
const result = await runDoctor(adapters, {
|
|
7624
7931
|
workspaceRoot,
|
|
7625
7932
|
skipAi: true,
|
|
7626
|
-
categories: ["cli", "config", "deps", "workspace"]
|
|
7933
|
+
categories: ["cli", "config", "deps", "workspace", "layers"]
|
|
7627
7934
|
});
|
|
7628
7935
|
for (const check of result.checks) {
|
|
7629
7936
|
if (check.status === "fail") {
|
|
@@ -7632,7 +7939,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7632
7939
|
severity: "error",
|
|
7633
7940
|
message: `${check.name}: ${check.message}`,
|
|
7634
7941
|
category: "doctor",
|
|
7635
|
-
context: { details: check.details }
|
|
7942
|
+
context: { details: check.details, ...check.context ?? {} }
|
|
7636
7943
|
});
|
|
7637
7944
|
} else if (check.status === "warn") {
|
|
7638
7945
|
issues.push({
|
|
@@ -7640,7 +7947,7 @@ async function runDoctorChecks(adapters, options) {
|
|
|
7640
7947
|
severity: "warning",
|
|
7641
7948
|
message: `${check.name}: ${check.message}`,
|
|
7642
7949
|
category: "doctor",
|
|
7643
|
-
context: { details: check.details }
|
|
7950
|
+
context: { details: check.details, ...check.context ?? {} }
|
|
7644
7951
|
});
|
|
7645
7952
|
}
|
|
7646
7953
|
}
|
|
@@ -8577,7 +8884,7 @@ init_config();
|
|
|
8577
8884
|
// src/services/drift.ts
|
|
8578
8885
|
import path4 from "path";
|
|
8579
8886
|
import { mkdtemp, rm as rm3 } from "node:fs/promises";
|
|
8580
|
-
import { tmpdir as
|
|
8887
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
8581
8888
|
|
|
8582
8889
|
// src/services/generate-artifacts.ts
|
|
8583
8890
|
import path3 from "path";
|
|
@@ -8737,7 +9044,7 @@ async function generateArtifacts(adapters, contractsDir, generatedDir, rootPath)
|
|
|
8737
9044
|
|
|
8738
9045
|
// src/services/drift.ts
|
|
8739
9046
|
async function detectDrift(adapters, contractsDir, generatedDir) {
|
|
8740
|
-
const tempDir = await mkdtemp(path4.join(
|
|
9047
|
+
const tempDir = await mkdtemp(path4.join(tmpdir3(), "contractspec-drift-"));
|
|
8741
9048
|
try {
|
|
8742
9049
|
await generateArtifacts(adapters, contractsDir, tempDir);
|
|
8743
9050
|
const differences = [];
|
|
@@ -16838,20 +17145,36 @@ function formatAsJson(result, options = {}) {
|
|
|
16838
17145
|
line: issue.line,
|
|
16839
17146
|
details: issue.context
|
|
16840
17147
|
}));
|
|
17148
|
+
const pass = result.categories.filter((category) => category.passed).length;
|
|
16841
17149
|
const fail = result.totalErrors;
|
|
16842
17150
|
const warn = result.totalWarnings;
|
|
17151
|
+
const note = result.totalNotes;
|
|
16843
17152
|
const output = {
|
|
16844
17153
|
schemaVersion: "1.0",
|
|
17154
|
+
success: result.success,
|
|
16845
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
|
+
})),
|
|
16846
17165
|
drift: {
|
|
16847
17166
|
status: driftResult?.hasDrift ? "detected" : "none",
|
|
16848
17167
|
files: driftResult?.files ?? []
|
|
16849
17168
|
},
|
|
16850
17169
|
summary: {
|
|
16851
|
-
pass
|
|
17170
|
+
pass,
|
|
16852
17171
|
fail,
|
|
16853
17172
|
warn,
|
|
16854
|
-
|
|
17173
|
+
note,
|
|
17174
|
+
total: pass + fail + warn + note,
|
|
17175
|
+
totalErrors: result.totalErrors,
|
|
17176
|
+
totalWarnings: result.totalWarnings,
|
|
17177
|
+
totalNotes: result.totalNotes,
|
|
16855
17178
|
durationMs: result.durationMs,
|
|
16856
17179
|
timestamp: result.timestamp
|
|
16857
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.3",
|
|
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.3",
|
|
37
|
+
"@contractspec/lib.ai-providers": "3.7.3",
|
|
38
|
+
"@contractspec/lib.contracts-spec": "3.7.3",
|
|
39
|
+
"@contractspec/lib.contracts-integrations": "3.7.3",
|
|
40
|
+
"@contractspec/lib.contracts-transformers": "3.7.3",
|
|
41
|
+
"@contractspec/lib.source-extractors": "2.7.3",
|
|
42
|
+
"@contractspec/module.workspace": "3.7.3",
|
|
43
|
+
"@contractspec/lib.utils-typescript": "3.7.3",
|
|
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.3",
|
|
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.3"
|
|
61
61
|
},
|
|
62
62
|
"exports": {
|
|
63
63
|
".": {
|