@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.
@@ -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: (newValue) => all[name] = () => newValue
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 { dirname as dirname3, join as join3, resolve as resolve4 } from "path";
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(tmpdir(), `cursor-agent-${Date.now()}`);
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(tmpdir(), `cursor-validate-${Date.now()}`);
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 nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
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
- if (ctx.isMonorepo && ctx.packageRoot === ctx.workspaceRoot && !config.outputDir) {
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, _ctx) {
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
- if (!feature.owners?.length) {
7073
- missingOwners.push(key);
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 tmpdir2 } from "os";
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(tmpdir2(), "contractspec-drift-"));
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: 0,
17170
+ pass,
16852
17171
  fail,
16853
17172
  warn,
16854
- total: fail + warn,
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
  },
@@ -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: (newValue) => all[name] = () => newValue
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 { dirname as dirname3, join as join3, resolve as resolve4 } from "node:path";
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(tmpdir(), `cursor-agent-${Date.now()}`);
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(tmpdir(), `cursor-validate-${Date.now()}`);
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 nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
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
- if (ctx.isMonorepo && ctx.packageRoot === ctx.workspaceRoot && !config.outputDir) {
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, _ctx) {
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
- if (!feature.owners?.length) {
7073
- missingOwners.push(key);
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 tmpdir2 } from "node:os";
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(tmpdir2(), "contractspec-drift-"));
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: 0,
17170
+ pass,
16852
17171
  fail,
16853
17172
  warn,
16854
- total: fail + warn,
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, _ctx: CheckContext): Promise<CheckResult[]>;
11
+ export declare function runLayerChecks(fs: FsAdapter, ctx: CheckContext): Promise<CheckResult[]>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { FsAdapter } from '../../../ports/fs';
2
+ import type { CheckContext, CheckResult } from '../types';
3
+ export declare function runPackageStabilityChecks(fs: FsAdapter, ctx: CheckContext): Promise<CheckResult[]>;
@@ -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.0",
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.0",
37
- "@contractspec/lib.ai-providers": "3.7.0",
38
- "@contractspec/lib.contracts-spec": "3.7.0",
39
- "@contractspec/lib.contracts-integrations": "3.7.0",
40
- "@contractspec/lib.contracts-transformers": "3.7.0",
41
- "@contractspec/lib.source-extractors": "2.7.0",
42
- "@contractspec/module.workspace": "3.7.0",
43
- "@contractspec/lib.utils-typescript": "3.7.0",
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.0",
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.0"
60
+ "@contractspec/tool.bun": "3.7.3"
61
61
  },
62
62
  "exports": {
63
63
  ".": {