@webpieces/nx-webpieces-rules 0.4.976 → 0.4.978

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.976",
3
+ "version": "0.4.978",
4
4
  "description": "Nx-specific webpieces validation rules, graph tooling, executors, and inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -20,12 +20,12 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@webpieces/api-doc-model": "0.4.976",
24
- "@webpieces/core-util": "0.4.976",
25
- "@webpieces/eslint-rules": "0.4.976",
26
- "@webpieces/rules-config": "0.4.976",
23
+ "@webpieces/api-doc-model": "0.4.978",
24
+ "@webpieces/core-util": "0.4.978",
25
+ "@webpieces/eslint-rules": "0.4.978",
26
+ "@webpieces/rules-config": "0.4.978",
27
27
  "madge": "8.0.0",
28
- "@webpieces/rules-sdk": "0.4.976"
28
+ "@webpieces/rules-sdk": "0.4.978"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@nx/devkit": ">=18.0.0"
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "devDependencies": {
44
- "@webpieces/tooling-testkit": "0.4.976"
44
+ "@webpieces/tooling-testkit": "0.4.978"
45
45
  },
46
46
  "types": "./src/index.d.ts",
47
47
  "bin": {
@@ -26,7 +26,7 @@ async function runExecutor(_options, context) {
26
26
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
27
27
  try {
28
28
  const projectInfos = await (0, graph_metadata_1.collectProjectInfo)();
29
- const scan = new api_contract_evidence_1.ApiContractEvidence().collect(workspaceRoot, projectInfos, (0, runtime_config_1.loadRuntimeConfig)(workspaceRoot).externalApiPaths);
29
+ const scan = await new api_contract_evidence_1.ApiContractEvidence().collect(workspaceRoot, projectInfos, (0, runtime_config_1.loadRuntimeConfig)(workspaceRoot).externalApiPaths);
30
30
  const violations = (0, api_lib_tag_validator_1.findApiLibTagViolations)(projectInfos, scan, workspaceRoot);
31
31
  if (violations.length === 0) {
32
32
  console.log('✅ role:api-lib / role:api-client match the code (every api library exports a contract or only wire types, and vice-versa).');
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-api-lib-tag/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAsBH,8BAuCC;AA3DD,0DAAgF;AAEhF,6DAA8D;AAC9D,qFAAgF;AAChF,6DAA6D;AAC7D,qFAGmD;AACnD,2CAAwC;AAUxC,uHAAuH;AACxG,KAAK,UAAU,WAAW,CACrC,QAAkC,EAClC,OAAwB;IAExB,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,6DAA6D,CAAC,CAAC;IAE3E,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,YAAY,GAAG,MAAM,IAAA,mCAAkB,GAAE,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,2CAAmB,EAAE,CAAC,OAAO,CAC1C,aAAa,EACb,YAAY,EACZ,IAAA,kCAAiB,EAAC,aAAa,CAAC,CAAC,gBAAgB,CACpD,CAAC;QACF,MAAM,UAAU,GAAG,IAAA,+CAAuB,EAAC,YAAY,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;QAE9E,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CACP,4HAA4H,CAC/H,CAAC;YACF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QAED,OAAO,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,MAAM,wCAAwC,CAAC,CAAC;QAC9E,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACjC,OAAO,CAAC,KAAK,CAAC,IAAA,kDAA0B,EAAC,SAAS,CAAC,CAAC,CAAC;YACrD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CACT,kCAAkC,EAClC,KAAK,YAAY,4BAAa,CAAC,CAAC,CAAC,IAAA,qCAAsB,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CACjF,CAAC;QACF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Validate API-lib Tag Executor\n *\n * Fails when the api roles and the code disagree, in either direction: a project\n * tagged role:api-lib that exports neither a contract (@ApiPath/@Rpc/@PubSub,\n * @WpInternal/@WpIpcEndpoint, or an in-process abstract …Api) nor only wire types,\n * a role:api-client that exports no contract, or a project that exports an\n * @ApiPath/@Rpc/@PubSub abstract class but carries neither api role (#1064, D3).\n *\n * Usage:\n * nx run architecture:validate-api-lib-tag\n */\n\nimport { RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport type { ExecutorContext } from '@nx/devkit';\nimport { collectProjectInfo } from '../../lib/graph-metadata';\nimport { ApiContractEvidence } from '../../lib/api-usage/api-contract-evidence';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport {\n findApiLibTagViolations,\n describeApiLibTagViolation,\n} from '../../lib/api-usage/api-lib-tag-validator';\nimport { toError } from '../../toError';\n\nexport interface ValidateApiLibTagOptions {\n // No options needed\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n// webpieces-disable no-function-outside-class -- nx executor entry point (default export), like every sibling executor\nexport default async function runExecutor(\n _options: ValidateApiLibTagOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n\n console.log('\\n🏷️ Validating role:api-lib tag ⇔ API contract exports\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const projectInfos = await collectProjectInfo();\n const scan = new ApiContractEvidence().collect(\n workspaceRoot,\n projectInfos,\n loadRuntimeConfig(workspaceRoot).externalApiPaths,\n );\n const violations = findApiLibTagViolations(projectInfos, scan, workspaceRoot);\n\n if (violations.length === 0) {\n console.log(\n '✅ role:api-lib / role:api-client match the code (every api library exports a contract or only wire types, and vice-versa).',\n );\n return { success: true };\n }\n\n console.error(`❌ ${violations.length} role:api-lib tag/code mismatch(es):\\n`);\n for (const violation of violations) {\n console.error(describeApiLibTagViolation(violation));\n console.error('');\n }\n return { success: false };\n } catch (err: unknown) {\n const error = toError(err);\n console.error(\n '❌ api-lib tag validation failed:',\n error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message,\n );\n return { success: false };\n }\n}\n"]}
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-api-lib-tag/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAsBH,8BAuCC;AA3DD,0DAAgF;AAEhF,6DAA8D;AAC9D,qFAAgF;AAChF,6DAA6D;AAC7D,qFAGmD;AACnD,2CAAwC;AAUxC,uHAAuH;AACxG,KAAK,UAAU,WAAW,CACrC,QAAkC,EAClC,OAAwB;IAExB,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,6DAA6D,CAAC,CAAC;IAE3E,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,YAAY,GAAG,MAAM,IAAA,mCAAkB,GAAE,CAAC;QAChD,MAAM,IAAI,GAAG,MAAM,IAAI,2CAAmB,EAAE,CAAC,OAAO,CAChD,aAAa,EACb,YAAY,EACZ,IAAA,kCAAiB,EAAC,aAAa,CAAC,CAAC,gBAAgB,CACpD,CAAC;QACF,MAAM,UAAU,GAAG,IAAA,+CAAuB,EAAC,YAAY,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;QAE9E,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CACP,4HAA4H,CAC/H,CAAC;YACF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QAED,OAAO,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,MAAM,wCAAwC,CAAC,CAAC;QAC9E,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACjC,OAAO,CAAC,KAAK,CAAC,IAAA,kDAA0B,EAAC,SAAS,CAAC,CAAC,CAAC;YACrD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CACT,kCAAkC,EAClC,KAAK,YAAY,4BAAa,CAAC,CAAC,CAAC,IAAA,qCAAsB,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CACjF,CAAC;QACF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Validate API-lib Tag Executor\n *\n * Fails when the api roles and the code disagree, in either direction: a project\n * tagged role:api-lib that exports neither a contract (@ApiPath/@Rpc/@PubSub,\n * @WpInternal/@WpIpcEndpoint, or an in-process abstract …Api) nor only wire types,\n * a role:api-client that exports no contract, or a project that exports an\n * @ApiPath/@Rpc/@PubSub abstract class but carries neither api role (#1064, D3).\n *\n * Usage:\n * nx run architecture:validate-api-lib-tag\n */\n\nimport { RuleFailError, renderRuleFailForHuman } from '@webpieces/rules-config';\nimport type { ExecutorContext } from '@nx/devkit';\nimport { collectProjectInfo } from '../../lib/graph-metadata';\nimport { ApiContractEvidence } from '../../lib/api-usage/api-contract-evidence';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport {\n findApiLibTagViolations,\n describeApiLibTagViolation,\n} from '../../lib/api-usage/api-lib-tag-validator';\nimport { toError } from '../../toError';\n\nexport interface ValidateApiLibTagOptions {\n // No options needed\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n// webpieces-disable no-function-outside-class -- nx executor entry point (default export), like every sibling executor\nexport default async function runExecutor(\n _options: ValidateApiLibTagOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n\n console.log('\\n🏷️ Validating role:api-lib tag ⇔ API contract exports\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const projectInfos = await collectProjectInfo();\n const scan = await new ApiContractEvidence().collect(\n workspaceRoot,\n projectInfos,\n loadRuntimeConfig(workspaceRoot).externalApiPaths,\n );\n const violations = findApiLibTagViolations(projectInfos, scan, workspaceRoot);\n\n if (violations.length === 0) {\n console.log(\n '✅ role:api-lib / role:api-client match the code (every api library exports a contract or only wire types, and vice-versa).',\n );\n return { success: true };\n }\n\n console.error(`❌ ${violations.length} role:api-lib tag/code mismatch(es):\\n`);\n for (const violation of violations) {\n console.error(describeApiLibTagViolation(violation));\n console.error('');\n }\n return { success: false };\n } catch (err: unknown) {\n const error = toError(err);\n console.error(\n '❌ api-lib tag validation failed:',\n error instanceof RuleFailError ? renderRuleFailForHuman(error) : error.message,\n );\n return { success: false };\n }\n}\n"]}
@@ -148,7 +148,7 @@ async function runModifiedFilesMode(workspaceRoot, excludePaths, allowedRootFile
148
148
  let base = process.env['NX_BASE'];
149
149
  const head = process.env['NX_HEAD'];
150
150
  if (!base) {
151
- base = (0, rules_config_1.detectBase)(workspaceRoot) ?? undefined;
151
+ base = (await (0, rules_config_1.detectBase)(workspaceRoot)) ?? undefined;
152
152
  if (!base) {
153
153
  console.log('\n⏭️ Skipping validate-ts-in-src validation (could not detect base branch)\n');
154
154
  return { success: true };
@@ -160,7 +160,7 @@ async function runModifiedFilesMode(workspaceRoot, excludePaths, allowedRootFile
160
160
  const projectRoots = await getProjectRoots(workspaceRoot);
161
161
  // git diff --name-only lists DELETED paths too (renames = delete+add);
162
162
  // a file that no longer exists cannot be an orphan - skip it
163
- const changedFiles = (0, rules_config_1.getChangedFiles)(workspaceRoot, base, head)
163
+ const changedFiles = (await (0, rules_config_1.getChangedFiles)(workspaceRoot, base, head))
164
164
  .filter((relPath) => fs.existsSync(path.join(workspaceRoot, relPath)));
165
165
  if (changedFiles.length === 0) {
166
166
  console.log('✅ No TypeScript files changed\n');
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-ts-in-src/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate TypeScript Files in src/ Executor\n *\n * Two-layer rule:\n * Layer 1: every .ts file inside an Nx project must live under src/\n * Layer 2: every .ts file anywhere in the workspace must belong to some\n * Nx project. Orphan files (at workspace root or in a non-project\n * directory) fail the rule unless explicitly allowlisted.\n *\n * A built-in floor (bare dir names + globs, e.g. **\\/*.d.ts and **\\/jest.config.ts) exempts files from\n * BOTH layers. The configured `excludePaths` — this rule's and the repo's top-level one — are the\n * framework-owned exclusions (#1253): the rule runs inside a RuleFileScope carrying them, so the changed-file\n * listing never contains a file they match. They ADD to the floor.\n *\n * Only the files changed vs the base branch are validated (NEW_AND_MODIFIED_FILES),\n * so the rule applies cleanly to legacy projects — pre-existing orphan files\n * are grandfathered and only newly-touched files are checked.\n *\n * Configurable via webpieces.config.json:\n * \"validate-ts-in-src\": {\n * \"mode\": \"NEW_AND_MODIFIED_FILES\", // \"OFF\" disables the rule\n * \"excludePaths\": [...], // globs, e.g. \"**\\/codegen.ts\", \"scripts/**\"\n * \"allowedRootFiles\": [...],\n * \"turnOffRuleUntilEpoch\": 0, // epoch SECONDS; skip the rule until it passes\n * \"turnOffRuleWhileOnBranch\": null // branch name; skip the rule while it is checked out\n * }\n *\n * Usage: nx run architecture:validate-ts-in-src\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph } from '@nx/devkit';\nimport {\n loadAndValidate,\n isPathExcluded,\n shouldSkipRule,\n detectBase,\n getChangedFiles,\n RuleFileExclusions,\n RuleFileScope,\n} from '@webpieces/rules-config';\nimport * as path from 'path';\nimport * as fs from 'fs';\n\nexport type ValidateTsInSrcMode = 'OFF' | 'NEW_AND_MODIFIED_FILES';\n\nexport interface ValidateTsInSrcOptions {\n mode?: ValidateTsInSrcMode;\n allowedRootFiles?: string[];\n turnOffRuleUntilEpoch?: number;\n turnOffRuleWhileOnBranch?: string | null;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst BUILT_IN_EXCLUDE_PATHS: string[] = [\n 'node_modules', 'dist', '.nx', '.git',\n 'architecture', 'tmp', 'scripts',\n '**/*.d.ts', '**/jest.config.ts',\n];\n\nconst DEFAULT_ALLOWED_ROOT_FILES: string[] = ['jest.setup.ts'];\n\nclass LayerOneViolation {\n filePath: string;\n projectName: string;\n\n constructor(filePath: string, projectName: string) {\n this.filePath = filePath;\n this.projectName = projectName;\n }\n}\n\nclass LayerTwoViolation {\n filePath: string;\n\n constructor(filePath: string) {\n this.filePath = filePath;\n }\n}\n\nasync function getProjectRoots(workspaceRoot: string): Promise<string[]> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n const roots: string[] = [];\n for (const cfg of Object.values(projectsConfig.projects)) {\n if (cfg.root === '' || cfg.root === '.') continue;\n if (cfg.root === 'architecture') continue;\n roots.push(path.join(workspaceRoot, cfg.root));\n }\n return roots;\n}\n\nfunction checkSingleFileLayerOne(\n relPath: string,\n projectRoots: string[],\n workspaceRoot: string,\n excludePaths: string[],\n): LayerOneViolation | null {\n if (isPathExcluded(relPath, excludePaths)) return null;\n const fullPath = path.join(workspaceRoot, relPath);\n for (const projectRoot of projectRoots) {\n if (fullPath.startsWith(projectRoot + path.sep) || fullPath === projectRoot) {\n const relToProject = path.relative(projectRoot, fullPath);\n if (relToProject.startsWith('src' + path.sep) || relToProject === 'src') return null;\n const projectName = path.relative(workspaceRoot, projectRoot);\n return new LayerOneViolation(relPath, projectName);\n }\n }\n return null; // not in any project — that's layer 2's concern\n}\n\nfunction checkSingleFileLayerTwo(\n relPath: string,\n projectRoots: string[],\n workspaceRoot: string,\n allowedRootFiles: string[],\n excludePaths: string[],\n): LayerTwoViolation | null {\n if (isPathExcluded(relPath, excludePaths)) return null;\n const parts = relPath.split(path.sep);\n if (parts.length === 1 && allowedRootFiles.includes(parts[0] ?? '')) return null;\n const fullPath = path.join(workspaceRoot, relPath);\n for (const projectRoot of projectRoots) {\n if (fullPath.startsWith(projectRoot + path.sep)) return null; // owned by a project\n }\n return new LayerTwoViolation(relPath);\n}\n\nfunction resolveMode(\n normalMode: ValidateTsInSrcMode,\n epoch: number | undefined,\n branch: string | undefined,\n): ValidateTsInSrcMode {\n if (normalMode === 'OFF') {\n return normalMode;\n }\n // Honor the universal escape hatches: skip while on a named branch or until epoch.\n const skip = shouldSkipRule(epoch, branch);\n if (skip.skip) {\n console.log(`\\n⏭️ Skipping validate-ts-in-src validation (${skip.reason})`);\n console.log('');\n return 'OFF';\n }\n return normalMode;\n}\n\nfunction reportLayerOneFailure(violations: LayerOneViolation[]): void {\n console.error('❌ TypeScript files found outside src/ directory!\\n');\n console.error('All .ts source files must be inside the project\\'s src/ directory.');\n console.error('This enforces the standard project structure:\\n');\n console.error(' packages/{category}/{name}/');\n console.error(' ├── src/ ← ALL .ts files here');\n console.error(' ├── package.json');\n console.error(' ├── project.json');\n console.error(' └── tsconfig.json\\n');\n\n for (const v of violations) {\n console.error(` ❌ ${v.filePath}`);\n }\n\n console.error('\\nTo fix, pick one:');\n console.error(' (a) Move the .ts file(s) into the src/ directory');\n console.error(' (b) Add a glob to validate-ts-in-src.excludePaths in');\n console.error(' webpieces.config.json (e.g. \"**/codegen.ts\"). Defaults already');\n console.error(' exempt **/*.d.ts and **/jest.config.ts.\\n');\n}\n\nfunction reportLayerTwoFailure(violations: LayerTwoViolation[]): void {\n console.error('❌ TypeScript files found outside any Nx project!\\n');\n console.error('Every .ts file must belong to an Nx project so it is compiled,');\n console.error('linted, and tested under a known project config. Orphan files are');\n console.error('invisible to the build graph and will rot.\\n');\n\n for (const v of violations) {\n console.error(` ❌ ${v.filePath}`);\n }\n\n console.error('\\nTo fix, pick one:');\n console.error(' (a) Move the file into an existing project\\'s src/ directory');\n console.error(' (b) Create a new project (add project.json) that owns the directory');\n console.error(' (c) Add a glob for the containing directory (e.g. \"tools/**\") to validate-ts-in-src.excludePaths');\n console.error(' in the validate-ts-in-src entry of webpieces.config.json (or its owner rule file), or to');\n console.error(' the top-level excludePaths for every rule; or add the filename to allowedRootFiles');\n console.error(' if it is a legitimate workspace-root file (e.g., jest.setup.ts)\\n');\n}\n\nasync function runModifiedFilesMode(\n workspaceRoot: string,\n excludePaths: string[],\n allowedRootFiles: string[],\n): Promise<ExecutorResult> {\n console.log('\\n📁 Validating TypeScript files are in src/ and owned by a project (NEW_AND_MODIFIED_FILES mode)\\n');\n\n let base = process.env['NX_BASE'];\n const head = process.env['NX_HEAD'];\n\n if (!base) {\n base = detectBase(workspaceRoot) ?? undefined;\n if (!base) {\n console.log('\\n⏭️ Skipping validate-ts-in-src validation (could not detect base branch)\\n');\n return { success: true };\n }\n }\n\n console.log(` Base: ${base}`);\n console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);\n console.log('');\n\n const projectRoots = await getProjectRoots(workspaceRoot);\n // git diff --name-only lists DELETED paths too (renames = delete+add);\n // a file that no longer exists cannot be an orphan - skip it\n const changedFiles = getChangedFiles(workspaceRoot, base, head)\n .filter((relPath: string) => fs.existsSync(path.join(workspaceRoot, relPath)));\n\n if (changedFiles.length === 0) {\n console.log('✅ No TypeScript files changed\\n');\n return { success: true };\n }\n\n console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);\n\n const layerOneViolations: LayerOneViolation[] = [];\n const layerTwoViolations: LayerTwoViolation[] = [];\n\n for (const relPath of changedFiles) {\n const l1 = checkSingleFileLayerOne(relPath, projectRoots, workspaceRoot, excludePaths);\n if (l1) layerOneViolations.push(l1);\n const l2 = checkSingleFileLayerTwo(relPath, projectRoots, workspaceRoot, allowedRootFiles, excludePaths);\n if (l2) layerTwoViolations.push(l2);\n }\n\n if (layerOneViolations.length === 0 && layerTwoViolations.length === 0) {\n console.log('✅ All changed .ts files are inside a project\\'s src/ directory\\n');\n return { success: true };\n }\n\n if (layerOneViolations.length > 0) {\n reportLayerOneFailure(layerOneViolations);\n }\n if (layerTwoViolations.length > 0) {\n reportLayerTwoFailure(layerTwoViolations);\n }\n\n console.error('To disable: set rules[\"validate-ts-in-src\"].mode to \"OFF\" in webpieces.config.json\\n');\n return { success: false };\n}\n\nexport default async function runExecutor(\n _nxOptions: ValidateTsInSrcOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n // Config comes from webpieces.config.json — same source as ai-hooks\n // and validate-code — via @webpieces/rules-config.\n const loaded = loadAndValidate(context.root);\n const shared = loaded.resolved;\n const rule = shared.rules.get('validate-ts-in-src');\n\n const rawMode = (rule?.options['mode'] as ValidateTsInSrcMode | undefined) ?? 'NEW_AND_MODIFIED_FILES';\n const epoch = rule?.options['turnOffRuleUntilEpoch'] as number | undefined;\n // turnOffRuleWhileOnBranch is required-but-NULLABLE in the config (null = \"always on\"), so narrow\n // to a branch NAME here; anything else means no branch scoping.\n const rawBranch = rule?.options['turnOffRuleWhileOnBranch'];\n const branch = typeof rawBranch === 'string' ? rawBranch : undefined;\n const effectiveMode = resolveMode(rawMode, epoch, branch);\n\n if (effectiveMode === 'OFF' || (rule && rule.isOff)) {\n console.log('\\n⏭️ Skipping validate-ts-in-src (mode: OFF)\\n');\n return { success: true };\n }\n\n const workspaceRoot = context.root;\n // The framework-owned exclusions (#1253): the repo's top-level list plus this rule's own.\n const exclusions = new RuleFileExclusions(\n 'validate-ts-in-src',\n loaded.excludePaths.paths,\n (rule?.options['excludePaths'] as string[] | undefined) ?? [],\n );\n const allowedRootFiles =\n (rule?.options['allowedRootFiles'] as string[] | undefined) ?? DEFAULT_ALLOWED_ROOT_FILES;\n\n // Only NEW_AND_MODIFIED_FILES remains once OFF is handled above — validate just the\n // files changed vs the base branch (legacy-friendly; no whole-workspace scan).\n return new RuleFileScope().runWithin(exclusions, () =>\n runModifiedFilesMode(workspaceRoot, BUILT_IN_EXCLUDE_PATHS, allowedRootFiles),\n );\n}\n"]}
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-ts-in-src/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate TypeScript Files in src/ Executor\n *\n * Two-layer rule:\n * Layer 1: every .ts file inside an Nx project must live under src/\n * Layer 2: every .ts file anywhere in the workspace must belong to some\n * Nx project. Orphan files (at workspace root or in a non-project\n * directory) fail the rule unless explicitly allowlisted.\n *\n * A built-in floor (bare dir names + globs, e.g. **\\/*.d.ts and **\\/jest.config.ts) exempts files from\n * BOTH layers. The configured `excludePaths` — this rule's and the repo's top-level one — are the\n * framework-owned exclusions (#1253): the rule runs inside a RuleFileScope carrying them, so the changed-file\n * listing never contains a file they match. They ADD to the floor.\n *\n * Only the files changed vs the base branch are validated (NEW_AND_MODIFIED_FILES),\n * so the rule applies cleanly to legacy projects — pre-existing orphan files\n * are grandfathered and only newly-touched files are checked.\n *\n * Configurable via webpieces.config.json:\n * \"validate-ts-in-src\": {\n * \"mode\": \"NEW_AND_MODIFIED_FILES\", // \"OFF\" disables the rule\n * \"excludePaths\": [...], // globs, e.g. \"**\\/codegen.ts\", \"scripts/**\"\n * \"allowedRootFiles\": [...],\n * \"turnOffRuleUntilEpoch\": 0, // epoch SECONDS; skip the rule until it passes\n * \"turnOffRuleWhileOnBranch\": null // branch name; skip the rule while it is checked out\n * }\n *\n * Usage: nx run architecture:validate-ts-in-src\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph } from '@nx/devkit';\nimport {\n loadAndValidate,\n isPathExcluded,\n shouldSkipRule,\n detectBase,\n getChangedFiles,\n RuleFileExclusions,\n RuleFileScope,\n} from '@webpieces/rules-config';\nimport * as path from 'path';\nimport * as fs from 'fs';\n\nexport type ValidateTsInSrcMode = 'OFF' | 'NEW_AND_MODIFIED_FILES';\n\nexport interface ValidateTsInSrcOptions {\n mode?: ValidateTsInSrcMode;\n allowedRootFiles?: string[];\n turnOffRuleUntilEpoch?: number;\n turnOffRuleWhileOnBranch?: string | null;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst BUILT_IN_EXCLUDE_PATHS: string[] = [\n 'node_modules', 'dist', '.nx', '.git',\n 'architecture', 'tmp', 'scripts',\n '**/*.d.ts', '**/jest.config.ts',\n];\n\nconst DEFAULT_ALLOWED_ROOT_FILES: string[] = ['jest.setup.ts'];\n\nclass LayerOneViolation {\n filePath: string;\n projectName: string;\n\n constructor(filePath: string, projectName: string) {\n this.filePath = filePath;\n this.projectName = projectName;\n }\n}\n\nclass LayerTwoViolation {\n filePath: string;\n\n constructor(filePath: string) {\n this.filePath = filePath;\n }\n}\n\nasync function getProjectRoots(workspaceRoot: string): Promise<string[]> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n const roots: string[] = [];\n for (const cfg of Object.values(projectsConfig.projects)) {\n if (cfg.root === '' || cfg.root === '.') continue;\n if (cfg.root === 'architecture') continue;\n roots.push(path.join(workspaceRoot, cfg.root));\n }\n return roots;\n}\n\nfunction checkSingleFileLayerOne(\n relPath: string,\n projectRoots: string[],\n workspaceRoot: string,\n excludePaths: string[],\n): LayerOneViolation | null {\n if (isPathExcluded(relPath, excludePaths)) return null;\n const fullPath = path.join(workspaceRoot, relPath);\n for (const projectRoot of projectRoots) {\n if (fullPath.startsWith(projectRoot + path.sep) || fullPath === projectRoot) {\n const relToProject = path.relative(projectRoot, fullPath);\n if (relToProject.startsWith('src' + path.sep) || relToProject === 'src') return null;\n const projectName = path.relative(workspaceRoot, projectRoot);\n return new LayerOneViolation(relPath, projectName);\n }\n }\n return null; // not in any project — that's layer 2's concern\n}\n\nfunction checkSingleFileLayerTwo(\n relPath: string,\n projectRoots: string[],\n workspaceRoot: string,\n allowedRootFiles: string[],\n excludePaths: string[],\n): LayerTwoViolation | null {\n if (isPathExcluded(relPath, excludePaths)) return null;\n const parts = relPath.split(path.sep);\n if (parts.length === 1 && allowedRootFiles.includes(parts[0] ?? '')) return null;\n const fullPath = path.join(workspaceRoot, relPath);\n for (const projectRoot of projectRoots) {\n if (fullPath.startsWith(projectRoot + path.sep)) return null; // owned by a project\n }\n return new LayerTwoViolation(relPath);\n}\n\nfunction resolveMode(\n normalMode: ValidateTsInSrcMode,\n epoch: number | undefined,\n branch: string | undefined,\n): ValidateTsInSrcMode {\n if (normalMode === 'OFF') {\n return normalMode;\n }\n // Honor the universal escape hatches: skip while on a named branch or until epoch.\n const skip = shouldSkipRule(epoch, branch);\n if (skip.skip) {\n console.log(`\\n⏭️ Skipping validate-ts-in-src validation (${skip.reason})`);\n console.log('');\n return 'OFF';\n }\n return normalMode;\n}\n\nfunction reportLayerOneFailure(violations: LayerOneViolation[]): void {\n console.error('❌ TypeScript files found outside src/ directory!\\n');\n console.error('All .ts source files must be inside the project\\'s src/ directory.');\n console.error('This enforces the standard project structure:\\n');\n console.error(' packages/{category}/{name}/');\n console.error(' ├── src/ ← ALL .ts files here');\n console.error(' ├── package.json');\n console.error(' ├── project.json');\n console.error(' └── tsconfig.json\\n');\n\n for (const v of violations) {\n console.error(` ❌ ${v.filePath}`);\n }\n\n console.error('\\nTo fix, pick one:');\n console.error(' (a) Move the .ts file(s) into the src/ directory');\n console.error(' (b) Add a glob to validate-ts-in-src.excludePaths in');\n console.error(' webpieces.config.json (e.g. \"**/codegen.ts\"). Defaults already');\n console.error(' exempt **/*.d.ts and **/jest.config.ts.\\n');\n}\n\nfunction reportLayerTwoFailure(violations: LayerTwoViolation[]): void {\n console.error('❌ TypeScript files found outside any Nx project!\\n');\n console.error('Every .ts file must belong to an Nx project so it is compiled,');\n console.error('linted, and tested under a known project config. Orphan files are');\n console.error('invisible to the build graph and will rot.\\n');\n\n for (const v of violations) {\n console.error(` ❌ ${v.filePath}`);\n }\n\n console.error('\\nTo fix, pick one:');\n console.error(' (a) Move the file into an existing project\\'s src/ directory');\n console.error(' (b) Create a new project (add project.json) that owns the directory');\n console.error(' (c) Add a glob for the containing directory (e.g. \"tools/**\") to validate-ts-in-src.excludePaths');\n console.error(' in the validate-ts-in-src entry of webpieces.config.json (or its owner rule file), or to');\n console.error(' the top-level excludePaths for every rule; or add the filename to allowedRootFiles');\n console.error(' if it is a legitimate workspace-root file (e.g., jest.setup.ts)\\n');\n}\n\nasync function runModifiedFilesMode(\n workspaceRoot: string,\n excludePaths: string[],\n allowedRootFiles: string[],\n): Promise<ExecutorResult> {\n console.log('\\n📁 Validating TypeScript files are in src/ and owned by a project (NEW_AND_MODIFIED_FILES mode)\\n');\n\n let base = process.env['NX_BASE'];\n const head = process.env['NX_HEAD'];\n\n if (!base) {\n base = (await detectBase(workspaceRoot)) ?? undefined;\n if (!base) {\n console.log('\\n⏭️ Skipping validate-ts-in-src validation (could not detect base branch)\\n');\n return { success: true };\n }\n }\n\n console.log(` Base: ${base}`);\n console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);\n console.log('');\n\n const projectRoots = await getProjectRoots(workspaceRoot);\n // git diff --name-only lists DELETED paths too (renames = delete+add);\n // a file that no longer exists cannot be an orphan - skip it\n const changedFiles = (await getChangedFiles(workspaceRoot, base, head))\n .filter((relPath: string) => fs.existsSync(path.join(workspaceRoot, relPath)));\n\n if (changedFiles.length === 0) {\n console.log('✅ No TypeScript files changed\\n');\n return { success: true };\n }\n\n console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);\n\n const layerOneViolations: LayerOneViolation[] = [];\n const layerTwoViolations: LayerTwoViolation[] = [];\n\n for (const relPath of changedFiles) {\n const l1 = checkSingleFileLayerOne(relPath, projectRoots, workspaceRoot, excludePaths);\n if (l1) layerOneViolations.push(l1);\n const l2 = checkSingleFileLayerTwo(relPath, projectRoots, workspaceRoot, allowedRootFiles, excludePaths);\n if (l2) layerTwoViolations.push(l2);\n }\n\n if (layerOneViolations.length === 0 && layerTwoViolations.length === 0) {\n console.log('✅ All changed .ts files are inside a project\\'s src/ directory\\n');\n return { success: true };\n }\n\n if (layerOneViolations.length > 0) {\n reportLayerOneFailure(layerOneViolations);\n }\n if (layerTwoViolations.length > 0) {\n reportLayerTwoFailure(layerTwoViolations);\n }\n\n console.error('To disable: set rules[\"validate-ts-in-src\"].mode to \"OFF\" in webpieces.config.json\\n');\n return { success: false };\n}\n\nexport default async function runExecutor(\n _nxOptions: ValidateTsInSrcOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n // Config comes from webpieces.config.json — same source as ai-hooks\n // and validate-code — via @webpieces/rules-config.\n const loaded = loadAndValidate(context.root);\n const shared = loaded.resolved;\n const rule = shared.rules.get('validate-ts-in-src');\n\n const rawMode = (rule?.options['mode'] as ValidateTsInSrcMode | undefined) ?? 'NEW_AND_MODIFIED_FILES';\n const epoch = rule?.options['turnOffRuleUntilEpoch'] as number | undefined;\n // turnOffRuleWhileOnBranch is required-but-NULLABLE in the config (null = \"always on\"), so narrow\n // to a branch NAME here; anything else means no branch scoping.\n const rawBranch = rule?.options['turnOffRuleWhileOnBranch'];\n const branch = typeof rawBranch === 'string' ? rawBranch : undefined;\n const effectiveMode = resolveMode(rawMode, epoch, branch);\n\n if (effectiveMode === 'OFF' || (rule && rule.isOff)) {\n console.log('\\n⏭️ Skipping validate-ts-in-src (mode: OFF)\\n');\n return { success: true };\n }\n\n const workspaceRoot = context.root;\n // The framework-owned exclusions (#1253): the repo's top-level list plus this rule's own.\n const exclusions = new RuleFileExclusions(\n 'validate-ts-in-src',\n loaded.excludePaths.paths,\n (rule?.options['excludePaths'] as string[] | undefined) ?? [],\n );\n const allowedRootFiles =\n (rule?.options['allowedRootFiles'] as string[] | undefined) ?? DEFAULT_ALLOWED_ROOT_FILES;\n\n // Only NEW_AND_MODIFIED_FILES remains once OFF is handled above — validate just the\n // files changed vs the base branch (legacy-friendly; no whole-workspace scan).\n return new RuleFileScope().runWithin(exclusions, () =>\n runModifiedFilesMode(workspaceRoot, BUILT_IN_EXCLUDE_PATHS, allowedRootFiles),\n );\n}\n"]}
@@ -2,5 +2,5 @@ import type { ApiScanResult } from './api-scanner';
2
2
  import type { ProjectInfo } from '../project-info';
3
3
  /** Build evidence for API/DTO correctness. No runtime relationship scan or project compiler loop. */
4
4
  export declare class ApiContractEvidence {
5
- collect(workspaceRoot: string, infos: Map<string, ProjectInfo>, externalApiPaths: readonly string[]): ApiScanResult;
5
+ collect(workspaceRoot: string, infos: Map<string, ProjectInfo>, externalApiPaths: readonly string[]): Promise<ApiScanResult>;
6
6
  }
@@ -14,7 +14,7 @@ const api_doc_rules_scan_1 = require("./api-doc-rules-scan");
14
14
  const wire_closure_1 = require("./wire-closure");
15
15
  /** Build evidence for API/DTO correctness. No runtime relationship scan or project compiler loop. */
16
16
  class ApiContractEvidence {
17
- collect(workspaceRoot, infos, externalApiPaths) {
17
+ async collect(workspaceRoot, infos, externalApiPaths) {
18
18
  const diagnostics = new api_ast_1.DecoratorArgDiagnostics(workspaceRoot);
19
19
  const index = new api_scanner_1.ApiSourceIndexBuilder(workspaceRoot, infos, externalApiPaths, diagnostics).build();
20
20
  const result = {
@@ -32,7 +32,7 @@ class ApiContractEvidence {
32
32
  undeclaredEndpointOperations: diagnostics.undeclaredEndpointOperations(),
33
33
  voidResponseMismatches: diagnostics.voidResponseMismatches(),
34
34
  rootUnions: new root_union_scan_1.RootUnionScan(workspaceRoot, infos, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot)).run(),
35
- apiDocRules: new api_doc_rules_scan_1.ApiDocRulesScan(workspaceRoot, infos, api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.OPENAPI_RULE), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.MCP_RULE), wire_closure_1.WireClosureRule.fromConfig(workspaceRoot)).run(),
35
+ apiDocRules: new api_doc_rules_scan_1.ApiDocRulesScan(workspaceRoot, infos, await api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.OPENAPI_RULE), await api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.MCP_RULE), await wire_closure_1.WireClosureRule.fromConfig(workspaceRoot)).run(),
36
36
  };
37
37
  new api_contract_approval_1.ApiContractApproval().verify(workspaceRoot, infos, (0, api_scanner_1.buildApiContracts)(result), (0, external_systems_1.buildExternalSystems)(index.byName, infos)); // Preserves the previous generator's API/DTO correctness failures.
38
38
  return result;
@@ -1 +1 @@
1
- {"version":3,"file":"api-contract-evidence.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-evidence.ts"],"names":[],"mappings":";;;;AAAA,mEAA8D;AAC9D,yDAA0D;AAC1D,+CAAyB;AACzB,mDAA6B;AAC7B,+CAAyE;AACzE,uCAAoD;AACpD,uDAAiE;AACjE,mDAAqE;AACrE,6DAAuD;AACvD,iDAAiD;AAIjD,qGAAqG;AACrG,MAAa,mBAAmB;IAC5B,OAAO,CACH,aAAqB,EACrB,KAA+B,EAC/B,gBAAmC;QAEnC,MAAM,WAAW,GAAG,IAAI,iCAAuB,CAAC,aAAa,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,IAAI,mCAAqB,CACnC,aAAa,EACb,KAAK,EACL,gBAAgB,EAChB,WAAW,CACd,CAAC,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAkB;YAC1B,kBAAkB,EAAE,IAAI,GAAG,EAAE;YAC7B,cAAc,EAAE,KAAK,CAAC,MAAM;YAC5B,QAAQ,EAAE,KAAK,CAAC,MAAM;YACtB,eAAe,EAAE,IAAI,GAAG,CACpB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;iBACd,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;iBAC3E,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAChC;YACD,kBAAkB,EAAE,EAAE;YACtB,uBAAuB,EAAE,WAAW,CAAC,GAAG,EAAE;YAC1C,uBAAuB,EAAE,WAAW,CAAC,uBAAuB,EAAE;YAC9D,mBAAmB,EAAE,WAAW,CAAC,gBAAgB,EAAE;YACnD,yBAAyB,EAAE,WAAW,CAAC,yBAAyB,EAAE;YAClE,4BAA4B,EAAE,WAAW,CAAC,4BAA4B,EAAE;YACxE,sBAAsB,EAAE,WAAW,CAAC,sBAAsB,EAAE;YAC5D,UAAU,EAAE,IAAI,+BAAa,CACzB,aAAa,EACb,KAAK,EACL,+BAAa,CAAC,UAAU,CAAC,aAAa,CAAC,CAC1C,CAAC,GAAG,EAAE;YACP,WAAW,EAAE,IAAI,oCAAe,CAC5B,aAAa,EACb,KAAK,EACL,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,4BAAY,CAAC,EAClD,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,wBAAQ,CAAC,EAC9C,8BAAe,CAAC,UAAU,CAAC,aAAa,CAAC,CAC5C,CAAC,GAAG,EAAE;SACV,CAAC;QACF,IAAI,2CAAmB,EAAE,CAAC,MAAM,CAC5B,aAAa,EACb,KAAK,EACL,IAAA,+BAAiB,EAAC,MAAM,CAAC,EACzB,IAAA,uCAAoB,EAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAC5C,CAAC,CAAC,mEAAmE;QACtE,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAlDD,kDAkDC","sourcesContent":["import { ApiContractApproval } from './api-contract-approval';\nimport { buildExternalSystems } from './external-systems';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ApiSourceIndexBuilder, buildApiContracts } from './api-scanner';\nimport { DecoratorArgDiagnostics } from './api-ast';\nimport { RootUnionRule, RootUnionScan } from './root-union-scan';\nimport { ApiDocRule, MCP_RULE, OPENAPI_RULE } from './api-doc-rules';\nimport { ApiDocRulesScan } from './api-doc-rules-scan';\nimport { WireClosureRule } from './wire-closure';\nimport type { ApiScanResult } from './api-scanner';\nimport type { ProjectInfo } from '../project-info';\n\n/** Build evidence for API/DTO correctness. No runtime relationship scan or project compiler loop. */\nexport class ApiContractEvidence {\n collect(\n workspaceRoot: string,\n infos: Map<string, ProjectInfo>,\n externalApiPaths: readonly string[],\n ): ApiScanResult {\n const diagnostics = new DecoratorArgDiagnostics(workspaceRoot);\n const index = new ApiSourceIndexBuilder(\n workspaceRoot,\n infos,\n externalApiPaths,\n diagnostics,\n ).build();\n const result: ApiScanResult = {\n relationsByProject: new Map(),\n apiLibProjects: index.owners,\n apiIndex: index.byName,\n scannedProjects: new Set(\n [...infos.values()]\n .filter((info) => fs.existsSync(path.join(workspaceRoot, info.root, 'src')))\n .map((info) => info.name),\n ),\n unresolvedApiCalls: [],\n nonLiteralDecoratorArgs: diagnostics.all(),\n unresolvedEndpointPaths: diagnostics.unresolvedEndpointPaths(),\n emptiedApiContracts: diagnostics.emptiedContracts(),\n undeclaredExternalCallers: diagnostics.undeclaredExternalCallers(),\n undeclaredEndpointOperations: diagnostics.undeclaredEndpointOperations(),\n voidResponseMismatches: diagnostics.voidResponseMismatches(),\n rootUnions: new RootUnionScan(\n workspaceRoot,\n infos,\n RootUnionRule.fromConfig(workspaceRoot),\n ).run(),\n apiDocRules: new ApiDocRulesScan(\n workspaceRoot,\n infos,\n ApiDocRule.fromConfig(workspaceRoot, OPENAPI_RULE),\n ApiDocRule.fromConfig(workspaceRoot, MCP_RULE),\n WireClosureRule.fromConfig(workspaceRoot),\n ).run(),\n };\n new ApiContractApproval().verify(\n workspaceRoot,\n infos,\n buildApiContracts(result),\n buildExternalSystems(index.byName, infos),\n ); // Preserves the previous generator's API/DTO correctness failures.\n return result;\n }\n}\n"]}
1
+ {"version":3,"file":"api-contract-evidence.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-evidence.ts"],"names":[],"mappings":";;;;AAAA,mEAA8D;AAC9D,yDAA0D;AAC1D,+CAAyB;AACzB,mDAA6B;AAC7B,+CAAyE;AACzE,uCAAoD;AACpD,uDAAiE;AACjE,mDAAqE;AACrE,6DAAuD;AACvD,iDAAiD;AAIjD,qGAAqG;AACrG,MAAa,mBAAmB;IAC5B,KAAK,CAAC,OAAO,CACT,aAAqB,EACrB,KAA+B,EAC/B,gBAAmC;QAEnC,MAAM,WAAW,GAAG,IAAI,iCAAuB,CAAC,aAAa,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,IAAI,mCAAqB,CACnC,aAAa,EACb,KAAK,EACL,gBAAgB,EAChB,WAAW,CACd,CAAC,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAkB;YAC1B,kBAAkB,EAAE,IAAI,GAAG,EAAE;YAC7B,cAAc,EAAE,KAAK,CAAC,MAAM;YAC5B,QAAQ,EAAE,KAAK,CAAC,MAAM;YACtB,eAAe,EAAE,IAAI,GAAG,CACpB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;iBACd,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;iBAC3E,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAChC;YACD,kBAAkB,EAAE,EAAE;YACtB,uBAAuB,EAAE,WAAW,CAAC,GAAG,EAAE;YAC1C,uBAAuB,EAAE,WAAW,CAAC,uBAAuB,EAAE;YAC9D,mBAAmB,EAAE,WAAW,CAAC,gBAAgB,EAAE;YACnD,yBAAyB,EAAE,WAAW,CAAC,yBAAyB,EAAE;YAClE,4BAA4B,EAAE,WAAW,CAAC,4BAA4B,EAAE;YACxE,sBAAsB,EAAE,WAAW,CAAC,sBAAsB,EAAE;YAC5D,UAAU,EAAE,IAAI,+BAAa,CACzB,aAAa,EACb,KAAK,EACL,+BAAa,CAAC,UAAU,CAAC,aAAa,CAAC,CAC1C,CAAC,GAAG,EAAE;YACP,WAAW,EAAE,IAAI,oCAAe,CAC5B,aAAa,EACb,KAAK,EACL,MAAM,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,4BAAY,CAAC,EACxD,MAAM,0BAAU,CAAC,UAAU,CAAC,aAAa,EAAE,wBAAQ,CAAC,EACpD,MAAM,8BAAe,CAAC,UAAU,CAAC,aAAa,CAAC,CAClD,CAAC,GAAG,EAAE;SACV,CAAC;QACF,IAAI,2CAAmB,EAAE,CAAC,MAAM,CAC5B,aAAa,EACb,KAAK,EACL,IAAA,+BAAiB,EAAC,MAAM,CAAC,EACzB,IAAA,uCAAoB,EAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAC5C,CAAC,CAAC,mEAAmE;QACtE,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAlDD,kDAkDC","sourcesContent":["import { ApiContractApproval } from './api-contract-approval';\nimport { buildExternalSystems } from './external-systems';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ApiSourceIndexBuilder, buildApiContracts } from './api-scanner';\nimport { DecoratorArgDiagnostics } from './api-ast';\nimport { RootUnionRule, RootUnionScan } from './root-union-scan';\nimport { ApiDocRule, MCP_RULE, OPENAPI_RULE } from './api-doc-rules';\nimport { ApiDocRulesScan } from './api-doc-rules-scan';\nimport { WireClosureRule } from './wire-closure';\nimport type { ApiScanResult } from './api-scanner';\nimport type { ProjectInfo } from '../project-info';\n\n/** Build evidence for API/DTO correctness. No runtime relationship scan or project compiler loop. */\nexport class ApiContractEvidence {\n async collect(\n workspaceRoot: string,\n infos: Map<string, ProjectInfo>,\n externalApiPaths: readonly string[],\n ): Promise<ApiScanResult> {\n const diagnostics = new DecoratorArgDiagnostics(workspaceRoot);\n const index = new ApiSourceIndexBuilder(\n workspaceRoot,\n infos,\n externalApiPaths,\n diagnostics,\n ).build();\n const result: ApiScanResult = {\n relationsByProject: new Map(),\n apiLibProjects: index.owners,\n apiIndex: index.byName,\n scannedProjects: new Set(\n [...infos.values()]\n .filter((info) => fs.existsSync(path.join(workspaceRoot, info.root, 'src')))\n .map((info) => info.name),\n ),\n unresolvedApiCalls: [],\n nonLiteralDecoratorArgs: diagnostics.all(),\n unresolvedEndpointPaths: diagnostics.unresolvedEndpointPaths(),\n emptiedApiContracts: diagnostics.emptiedContracts(),\n undeclaredExternalCallers: diagnostics.undeclaredExternalCallers(),\n undeclaredEndpointOperations: diagnostics.undeclaredEndpointOperations(),\n voidResponseMismatches: diagnostics.voidResponseMismatches(),\n rootUnions: new RootUnionScan(\n workspaceRoot,\n infos,\n RootUnionRule.fromConfig(workspaceRoot),\n ).run(),\n apiDocRules: new ApiDocRulesScan(\n workspaceRoot,\n infos,\n await ApiDocRule.fromConfig(workspaceRoot, OPENAPI_RULE),\n await ApiDocRule.fromConfig(workspaceRoot, MCP_RULE),\n await WireClosureRule.fromConfig(workspaceRoot),\n ).run(),\n };\n new ApiContractApproval().verify(\n workspaceRoot,\n infos,\n buildApiContracts(result),\n buildExternalSystems(index.byName, infos),\n ); // Preserves the previous generator's API/DTO correctness failures.\n return result;\n }\n}\n"]}
@@ -148,7 +148,7 @@ export declare class ApiDocRule {
148
148
  */
149
149
  coversProject(root: string): boolean;
150
150
  /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */
151
- static fromConfig(workspaceRoot: string, name: string): ApiDocRule;
151
+ static fromConfig(workspaceRoot: string, name: string): Promise<ApiDocRule>;
152
152
  /**
153
153
  * Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base
154
154
  * with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's
@@ -195,7 +195,7 @@ class ApiDocRule {
195
195
  }
196
196
  /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */
197
197
  // webpieces-disable no-function-outside-class -- static factory of this class
198
- static fromConfig(workspaceRoot, name) {
198
+ static async fromConfig(workspaceRoot, name) {
199
199
  if (new rule_gate_1.RuleGate().isDisabled(workspaceRoot, name, true)) {
200
200
  return ApiDocRule.off(name);
201
201
  }
@@ -203,7 +203,7 @@ class ApiDocRule {
203
203
  const allowed = rule?.options['allowedPaths'];
204
204
  const allowedPaths = Array.isArray(allowed) ? allowed : [];
205
205
  const affected = rule?.options['mode'] === exports.AFFECTED_PROJECT_MODE
206
- ? ApiDocRule.changedPathsOf(workspaceRoot)
206
+ ? await ApiDocRule.changedPathsOf(workspaceRoot)
207
207
  : null;
208
208
  return new ApiDocRule(name, true, allowedPaths, affected);
209
209
  }
@@ -214,9 +214,9 @@ class ApiDocRule {
214
214
  * project owns a contract at all.
215
215
  */
216
216
  // webpieces-disable no-function-outside-class -- private static reader of this class
217
- static changedPathsOf(workspaceRoot) {
217
+ static async changedPathsOf(workspaceRoot) {
218
218
  const scope = new rules_config_1.DiffScope();
219
- const range = scope.resolveBase(workspaceRoot);
219
+ const range = await scope.resolveBase(workspaceRoot);
220
220
  if (range.base === undefined)
221
221
  return null;
222
222
  const opts = new rules_config_1.ChangedFilesOptions();
@@ -1 +1 @@
1
- {"version":3,"file":"api-doc-rules.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).\n *\n * The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the\n * per-rule config, so a unit test can construct either without touching a config file and so the\n * config is read exactly once per executor run.\n */\n\nimport * as fs from 'fs';\nimport { ChangedFilesOptions, DiffScope, loadAndValidate, matchesAnyGlob, WEBPIECES_DISABLE } from '@webpieces/rules-config';\nimport { RuleGate } from '../rule-gate';\n\n/** As written in a disable comment and as a config key. */\nexport const OPENAPI_RULE = \"api-rules-for-openapi\";\nexport const MCP_RULE = \"api-rules-for-mcp\";\n\n/** The `@ApiType` value that means \"a partner reads this document\". */\nexport const EXTERNAL_CUSTOMER_API_TYPE = 'external-customer';\n\n/**\n * `// webpieces-disable <rule>[, <rule2>] -- <reason>`, with the reason CAPTURED so a reasonless\n * disable can be told apart from an absent one. Identical to the spelling `root-union-scan` reads.\n */\nconst DISABLE_RE = new RegExp(\n `//\\\\s*${WEBPIECES_DISABLE}\\\\s+([\\\\w-]+(?:\\\\s*,\\\\s*[\\\\w-]+)*)(?:\\\\s*--\\\\s*(.*))?$`,\n);\n\n/** How far above a declaration a disable comment may sit before it is somebody else's comment. */\nconst DISABLE_LOOKBACK_LINES = 40;\n\n/**\n * ONE defect on ONE contract.\n *\n * `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the\n * SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a\n * partner document costs a partner something, where the same field on a service-to-service contract\n * costs a colleague a question. The refusal sorts and labels on it.\n */\nexport class ApiContractDefect {\n constructor(\n /** The `@ApiPath` contract class. */\n public readonly api: string,\n /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */\n public readonly method: string,\n /** What is wrong, in one line, naming the declaration. */\n public readonly what: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n /** What to do instead, in one sentence. */\n public readonly cure: string,\n /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */\n public readonly exposure: readonly string[],\n ) {}\n\n /** True when this contract feeds the PARTNER-facing document. */\n isExternal(): boolean {\n return this.exposure.includes(EXTERNAL_CUSTOMER_API_TYPE);\n }\n\n /** `Api.method` or `Api` — what a reader opens. */\n where(): string {\n return this.method === '' ? this.api : `${this.api}.${this.method}`;\n }\n}\n\n/** What ONE rule found: real defects, and disables of it that gave no reason. */\nexport class ApiRuleFindings {\n constructor(\n public readonly violations: readonly ApiContractDefect[],\n /**\n * Sites that named this rule in a disable and wrote no reason. A reasonless disable is\n * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only\n * part the next reader can weigh.\n */\n public readonly reasonlessDisables: readonly ApiContractDefect[],\n ) {}\n\n isEmpty(): boolean {\n return this.violations.length === 0 && this.reasonlessDisables.length === 0;\n }\n}\n\n/**\n * ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).\n *\n * Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the\n * findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every\n * run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read\n * again, and a build-time warning at add time would be read exactly as little.\n */\nexport class McpExclusion {\n constructor(\n public readonly api: string,\n public readonly method: string,\n /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */\n public readonly reason: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Both rules' findings from ONE scan — one program build, two verdicts. */\nexport class ApiDocRulesFindings {\n constructor(\n public readonly openApi: ApiRuleFindings,\n public readonly mcp: ApiRuleFindings,\n /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */\n public readonly mcpExclusions: readonly McpExclusion[] = [],\n ) {}\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static empty(): ApiDocRulesFindings {\n return new ApiDocRulesFindings(\n new ApiRuleFindings([], []),\n new ApiRuleFindings([], []),\n [],\n );\n }\n}\n\n/** The mode value that narrows the scan to the projects the diff touched. */\nexport const AFFECTED_PROJECT_MODE = 'AFFECTED_PROJECT';\n\n/**\n * ONE rule's switches, resolved from webpieces.config.json once per scan.\n *\n * There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an\n * entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,\n * `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test\n * gets is not a default; it is what a caller that configured nothing asked for.\n */\nexport class ApiDocRule {\n constructor(\n public readonly name: string,\n public readonly enabled: boolean,\n /** Project roots this rule does not apply to — `allowedPaths` in the config. */\n public readonly allowedPaths: readonly string[],\n /**\n * Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under\n * `RUN_EVERY_TIME`, which means every project is in scope.\n *\n * `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no\n * repository at all. A diff that could not be read is not evidence that nothing changed, and\n * the only safe reading of \"I do not know what changed\" is \"look at all of it\".\n */\n public readonly changedPaths: readonly string[] | null = null,\n ) {}\n\n /** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static armed(name: string): ApiDocRule {\n return new ApiDocRule(name, true, [], null);\n }\n\n /** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static affected(name: string, changedPaths: readonly string[]): ApiDocRule {\n return new ApiDocRule(name, true, [], changedPaths);\n }\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static off(name: string): ApiDocRule {\n return new ApiDocRule(name, false, [], null);\n }\n\n /**\n * True when this rule scans the contracts of the project rooted at `root`.\n *\n * ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can\n * never be applied by one caller and skipped by another.\n */\n coversProject(root: string): boolean {\n if (!this.enabled) return false;\n if (matchesAnyGlob(root, this.allowedPaths)) return false;\n if (this.changedPaths === null) return true;\n const prefix = `${root}/`;\n return this.changedPaths.some((each: string): boolean => each.startsWith(prefix));\n }\n\n /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromConfig(workspaceRoot: string, name: string): ApiDocRule {\n if (new RuleGate().isDisabled(workspaceRoot, name, true)) {\n return ApiDocRule.off(name);\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get(name);\n const allowed = rule?.options['allowedPaths'];\n const allowedPaths = Array.isArray(allowed) ? (allowed as string[]) : [];\n const affected =\n rule?.options['mode'] === AFFECTED_PROJECT_MODE\n ? ApiDocRule.changedPathsOf(workspaceRoot)\n : null;\n return new ApiDocRule(name, true, allowedPaths, affected);\n }\n\n /**\n * Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base\n * with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's\n * contracts can express exactly as an added one does, and a `project.json` edit can change which\n * project owns a contract at all.\n */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static changedPathsOf(workspaceRoot: string): readonly string[] | null {\n const scope = new DiffScope();\n const range = scope.resolveBase(workspaceRoot);\n if (range.base === undefined) return null;\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n return scope.getChangedFiles(workspaceRoot, range.base, range.head, opts);\n }\n}\n\n/** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */\nexport class DisableComment {\n constructor(public readonly hasReason: boolean) {}\n\n /**\n * The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.\n *\n * It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a\n * JSDoc block, and the decorators between them — because that is where an author writes one, and\n * it stops at the first line that is none of those, so a directive belonging to the PREVIOUS\n * declaration can never be read as covering this one.\n *\n * Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`\n * therefore does not silence these rules, which is the point: that rule answers \"is this\n * type-safe?\" and these answer \"is this field PUBLISHED with no shape?\" — different questions\n * with different right answers, so the second one wants its own, separately argued line.\n *\n * A file that cannot be read yields \"no disable\": a defect is still a defect, and inventing a\n * suppression out of an I/O failure is the one wrong answer.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static readAt(file: string, line: number, rule: string): DisableComment | undefined {\n const lines = DisableComment.linesOf(file);\n if (lines === undefined) return undefined;\n const start = Math.max(0, line - 1);\n const onDeclaration = DisableComment.match(lines[start] ?? '', rule);\n if (onDeclaration !== undefined) return onDeclaration;\n for (let i = start - 1; i >= 0 && i >= start - DISABLE_LOOKBACK_LINES; i--) {\n const text = (lines[i] ?? '').trim();\n const above = DisableComment.match(text, rule);\n if (above !== undefined) return above;\n if (!DisableComment.isTrivia(text)) return undefined;\n }\n return undefined;\n }\n\n /** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */\n // webpieces-disable no-function-outside-class -- private static predicate of this class\n private static isTrivia(text: string): boolean {\n return (\n text === '' ||\n text.startsWith('//') ||\n text.startsWith('*') ||\n text.startsWith('/*') ||\n text.startsWith('@')\n );\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static match(text: string, rule: string): DisableComment | undefined {\n const found = text.trim().match(DISABLE_RE);\n if (found === null) return undefined;\n const named = found[1].split(',').map((each: string): string => each.trim());\n if (!named.includes(rule)) return undefined;\n return new DisableComment((found[2] ?? '').trim() !== '');\n }\n\n /** The file's lines, or undefined when it cannot be read. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static linesOf(file: string): string[] | undefined {\n if (!fs.existsSync(file)) return undefined;\n return fs.readFileSync(file, 'utf8').split('\\n');\n }\n}\n"]}
1
+ {"version":3,"file":"api-doc-rules.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).\n *\n * The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the\n * per-rule config, so a unit test can construct either without touching a config file and so the\n * config is read exactly once per executor run.\n */\n\nimport * as fs from 'fs';\nimport { ChangedFilesOptions, DiffScope, loadAndValidate, matchesAnyGlob, WEBPIECES_DISABLE } from '@webpieces/rules-config';\nimport { RuleGate } from '../rule-gate';\n\n/** As written in a disable comment and as a config key. */\nexport const OPENAPI_RULE = \"api-rules-for-openapi\";\nexport const MCP_RULE = \"api-rules-for-mcp\";\n\n/** The `@ApiType` value that means \"a partner reads this document\". */\nexport const EXTERNAL_CUSTOMER_API_TYPE = 'external-customer';\n\n/**\n * `// webpieces-disable <rule>[, <rule2>] -- <reason>`, with the reason CAPTURED so a reasonless\n * disable can be told apart from an absent one. Identical to the spelling `root-union-scan` reads.\n */\nconst DISABLE_RE = new RegExp(\n `//\\\\s*${WEBPIECES_DISABLE}\\\\s+([\\\\w-]+(?:\\\\s*,\\\\s*[\\\\w-]+)*)(?:\\\\s*--\\\\s*(.*))?$`,\n);\n\n/** How far above a declaration a disable comment may sit before it is somebody else's comment. */\nconst DISABLE_LOOKBACK_LINES = 40;\n\n/**\n * ONE defect on ONE contract.\n *\n * `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the\n * SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a\n * partner document costs a partner something, where the same field on a service-to-service contract\n * costs a colleague a question. The refusal sorts and labels on it.\n */\nexport class ApiContractDefect {\n constructor(\n /** The `@ApiPath` contract class. */\n public readonly api: string,\n /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */\n public readonly method: string,\n /** What is wrong, in one line, naming the declaration. */\n public readonly what: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n /** What to do instead, in one sentence. */\n public readonly cure: string,\n /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */\n public readonly exposure: readonly string[],\n ) {}\n\n /** True when this contract feeds the PARTNER-facing document. */\n isExternal(): boolean {\n return this.exposure.includes(EXTERNAL_CUSTOMER_API_TYPE);\n }\n\n /** `Api.method` or `Api` — what a reader opens. */\n where(): string {\n return this.method === '' ? this.api : `${this.api}.${this.method}`;\n }\n}\n\n/** What ONE rule found: real defects, and disables of it that gave no reason. */\nexport class ApiRuleFindings {\n constructor(\n public readonly violations: readonly ApiContractDefect[],\n /**\n * Sites that named this rule in a disable and wrote no reason. A reasonless disable is\n * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only\n * part the next reader can weigh.\n */\n public readonly reasonlessDisables: readonly ApiContractDefect[],\n ) {}\n\n isEmpty(): boolean {\n return this.violations.length === 0 && this.reasonlessDisables.length === 0;\n }\n}\n\n/**\n * ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).\n *\n * Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the\n * findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every\n * run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read\n * again, and a build-time warning at add time would be read exactly as little.\n */\nexport class McpExclusion {\n constructor(\n public readonly api: string,\n public readonly method: string,\n /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */\n public readonly reason: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Both rules' findings from ONE scan — one program build, two verdicts. */\nexport class ApiDocRulesFindings {\n constructor(\n public readonly openApi: ApiRuleFindings,\n public readonly mcp: ApiRuleFindings,\n /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */\n public readonly mcpExclusions: readonly McpExclusion[] = [],\n ) {}\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static empty(): ApiDocRulesFindings {\n return new ApiDocRulesFindings(\n new ApiRuleFindings([], []),\n new ApiRuleFindings([], []),\n [],\n );\n }\n}\n\n/** The mode value that narrows the scan to the projects the diff touched. */\nexport const AFFECTED_PROJECT_MODE = 'AFFECTED_PROJECT';\n\n/**\n * ONE rule's switches, resolved from webpieces.config.json once per scan.\n *\n * There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an\n * entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,\n * `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test\n * gets is not a default; it is what a caller that configured nothing asked for.\n */\nexport class ApiDocRule {\n constructor(\n public readonly name: string,\n public readonly enabled: boolean,\n /** Project roots this rule does not apply to — `allowedPaths` in the config. */\n public readonly allowedPaths: readonly string[],\n /**\n * Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under\n * `RUN_EVERY_TIME`, which means every project is in scope.\n *\n * `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no\n * repository at all. A diff that could not be read is not evidence that nothing changed, and\n * the only safe reading of \"I do not know what changed\" is \"look at all of it\".\n */\n public readonly changedPaths: readonly string[] | null = null,\n ) {}\n\n /** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static armed(name: string): ApiDocRule {\n return new ApiDocRule(name, true, [], null);\n }\n\n /** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static affected(name: string, changedPaths: readonly string[]): ApiDocRule {\n return new ApiDocRule(name, true, [], changedPaths);\n }\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static off(name: string): ApiDocRule {\n return new ApiDocRule(name, false, [], null);\n }\n\n /**\n * True when this rule scans the contracts of the project rooted at `root`.\n *\n * ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can\n * never be applied by one caller and skipped by another.\n */\n coversProject(root: string): boolean {\n if (!this.enabled) return false;\n if (matchesAnyGlob(root, this.allowedPaths)) return false;\n if (this.changedPaths === null) return true;\n const prefix = `${root}/`;\n return this.changedPaths.some((each: string): boolean => each.startsWith(prefix));\n }\n\n /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static async fromConfig(workspaceRoot: string, name: string): Promise<ApiDocRule> {\n if (new RuleGate().isDisabled(workspaceRoot, name, true)) {\n return ApiDocRule.off(name);\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get(name);\n const allowed = rule?.options['allowedPaths'];\n const allowedPaths = Array.isArray(allowed) ? (allowed as string[]) : [];\n const affected =\n rule?.options['mode'] === AFFECTED_PROJECT_MODE\n ? await ApiDocRule.changedPathsOf(workspaceRoot)\n : null;\n return new ApiDocRule(name, true, allowedPaths, affected);\n }\n\n /**\n * Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base\n * with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's\n * contracts can express exactly as an added one does, and a `project.json` edit can change which\n * project owns a contract at all.\n */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static async changedPathsOf(workspaceRoot: string): Promise<readonly string[] | null> {\n const scope = new DiffScope();\n const range = await scope.resolveBase(workspaceRoot);\n if (range.base === undefined) return null;\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n return scope.getChangedFiles(workspaceRoot, range.base, range.head, opts);\n }\n}\n\n/** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */\nexport class DisableComment {\n constructor(public readonly hasReason: boolean) {}\n\n /**\n * The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.\n *\n * It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a\n * JSDoc block, and the decorators between them — because that is where an author writes one, and\n * it stops at the first line that is none of those, so a directive belonging to the PREVIOUS\n * declaration can never be read as covering this one.\n *\n * Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`\n * therefore does not silence these rules, which is the point: that rule answers \"is this\n * type-safe?\" and these answer \"is this field PUBLISHED with no shape?\" — different questions\n * with different right answers, so the second one wants its own, separately argued line.\n *\n * A file that cannot be read yields \"no disable\": a defect is still a defect, and inventing a\n * suppression out of an I/O failure is the one wrong answer.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static readAt(file: string, line: number, rule: string): DisableComment | undefined {\n const lines = DisableComment.linesOf(file);\n if (lines === undefined) return undefined;\n const start = Math.max(0, line - 1);\n const onDeclaration = DisableComment.match(lines[start] ?? '', rule);\n if (onDeclaration !== undefined) return onDeclaration;\n for (let i = start - 1; i >= 0 && i >= start - DISABLE_LOOKBACK_LINES; i--) {\n const text = (lines[i] ?? '').trim();\n const above = DisableComment.match(text, rule);\n if (above !== undefined) return above;\n if (!DisableComment.isTrivia(text)) return undefined;\n }\n return undefined;\n }\n\n /** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */\n // webpieces-disable no-function-outside-class -- private static predicate of this class\n private static isTrivia(text: string): boolean {\n return (\n text === '' ||\n text.startsWith('//') ||\n text.startsWith('*') ||\n text.startsWith('/*') ||\n text.startsWith('@')\n );\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static match(text: string, rule: string): DisableComment | undefined {\n const found = text.trim().match(DISABLE_RE);\n if (found === null) return undefined;\n const named = found[1].split(',').map((each: string): string => each.trim());\n if (!named.includes(rule)) return undefined;\n return new DisableComment((found[2] ?? '').trim() !== '');\n }\n\n /** The file's lines, or undefined when it cannot be read. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static linesOf(file: string): string[] | undefined {\n if (!fs.existsSync(file)) return undefined;\n return fs.readFileSync(file, 'utf8').split('\\n');\n }\n}\n"]}
@@ -200,7 +200,7 @@ export declare class ApiUsageScanner {
200
200
  * and must attach the SAME field, or it would see a phantom diff). Returns the
201
201
  * full scan so callers (validators, runtime graph) can reuse the api index.
202
202
  */
203
- export declare function scanAndAttachApiRelations(workspaceRoot: string, graph: EnhancedGraph, projectInfos: Map<string, ProjectInfo>, externalApiPaths?: readonly string[]): ApiScanResult;
203
+ export declare function scanAndAttachApiRelations(workspaceRoot: string, graph: EnhancedGraph, projectInfos: Map<string, ProjectInfo>, externalApiPaths?: readonly string[]): Promise<ApiScanResult>;
204
204
  /**
205
205
  * The committed api contract table (one `architecture/apis/<ApiName>.json` per entry), from a
206
206
  * completed scan.
@@ -409,8 +409,8 @@ exports.ApiUsageScanner = ApiUsageScanner;
409
409
  * full scan so callers (validators, runtime graph) can reuse the api index.
410
410
  */
411
411
  // webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings
412
- function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths = []) {
413
- const result = new ApiUsageScanner(workspaceRoot, projectInfos, externalApiPaths, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.OPENAPI_RULE), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.MCP_RULE), wire_closure_1.WireClosureRule.fromConfig(workspaceRoot)).scan();
412
+ async function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths = []) {
413
+ const result = new ApiUsageScanner(workspaceRoot, projectInfos, externalApiPaths, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot), await api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.OPENAPI_RULE), await api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.MCP_RULE), await wire_closure_1.WireClosureRule.fromConfig(workspaceRoot)).scan();
414
414
  for (const projectName of result.relationsByProject.keys()) {
415
415
  const entry = graph[projectName];
416
416
  if (entry)