From f07758c557a56a0bdf939271e208e35e4ed34527 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:00:34 +0000 Subject: [PATCH 1/4] Expand effect-ts compliance checks --- packages/effect-ts-check/package.json | 4 + packages/effect-ts-check/src/base.mjs | 6 + packages/effect-ts-check/src/index.mjs | 5 + .../effect-ts-check/src/rules/imports.mjs | 14 ++ packages/effect-ts-check/src/rules/index.mjs | 14 +- packages/effect-ts-check/src/rules/syntax.mjs | 24 +++- packages/effect-ts-check/src/run.mjs | 17 ++- .../effect-ts-check/src/strict-format.mjs | 10 ++ packages/effect-ts-check/src/strict.mjs | 73 +++++++++-- packages/effect-ts-check/tests/cli.test.mjs | 17 +++ .../effect-ts-check/tests/configs.test.mjs | 120 +++++++++++++++++- .../skills/effect-ts-guide/SKILL.md | 18 ++- .../prover-coder-ai-effect-ts-check-0.1.0.tgz | Bin 3371 -> 3940 bytes .../effect-ts-guide/references/lint-checks.md | 21 ++- pnpm-lock.yaml | 15 +++ tools/validate-distribution.mjs | 13 +- 16 files changed, 347 insertions(+), 24 deletions(-) create mode 100644 packages/effect-ts-check/src/strict-format.mjs diff --git a/packages/effect-ts-check/package.json b/packages/effect-ts-check/package.json index 1120e52..0bffdb1 100644 --- a/packages/effect-ts-check/package.json +++ b/packages/effect-ts-check/package.json @@ -18,6 +18,9 @@ "./strict": { "import": "./src/strict.mjs" }, + "./strict-format": { + "import": "./src/strict-format.mjs" + }, "./rules": { "import": "./src/rules/index.mjs" }, @@ -42,6 +45,7 @@ }, "dependencies": { "@effect/eslint-plugin": "^0.3.2", + "@eslint-community/eslint-plugin-eslint-comments": "^4.7.2", "@eslint/js": "^10.0.0", "eslint": "^10.0.0", "typescript-eslint": "^8.57.1" diff --git a/packages/effect-ts-check/src/base.mjs b/packages/effect-ts-check/src/base.mjs index f48c064..e4c0866 100644 --- a/packages/effect-ts-check/src/base.mjs +++ b/packages/effect-ts-check/src/base.mjs @@ -3,6 +3,12 @@ import tseslint from "typescript-eslint"; import { effectSyntaxRestrictions } from "./rules/index.mjs"; export const effectFileGlobs = Object.freeze(["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"]); +export const effectCoreFileGlobs = Object.freeze([ + "**/src/core/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}", +]); +export const effectCoreAxiomsFileGlobs = Object.freeze([ + "**/src/core/axioms.{ts,tsx,mts,cts}", +]); export const effectIgnoreGlobs = Object.freeze([ "tests/**", "**/tests/**", diff --git a/packages/effect-ts-check/src/index.mjs b/packages/effect-ts-check/src/index.mjs index 78d4c08..bf84c85 100644 --- a/packages/effect-ts-check/src/index.mjs +++ b/packages/effect-ts-check/src/index.mjs @@ -1,10 +1,15 @@ export { minimal } from "./minimal.mjs"; export { + effectCoreRestrictedImportPatterns, effectRestrictedImportPatterns, effectRestrictedImports, + effectCoreSyntaxRestrictions, + effectErrorBoundarySyntaxRestrictions, + effectHostSyntaxRestrictions, effectStrictSyntaxRestrictions, effectSyntaxRestrictions, effectTypeRules, } from "./rules/index.mjs"; export { getProfileConfig, lintPaths, main, parseArguments } from "./run.mjs"; export { strict } from "./strict.mjs"; +export { strictFormat } from "./strict-format.mjs"; diff --git a/packages/effect-ts-check/src/rules/imports.mjs b/packages/effect-ts-check/src/rules/imports.mjs index 476a1a6..d3dec3a 100644 --- a/packages/effect-ts-check/src/rules/imports.mjs +++ b/packages/effect-ts-check/src/rules/imports.mjs @@ -55,3 +55,17 @@ export const effectRestrictedImportPatterns = Object.freeze([ message: "Do not import from node:* directly. Use @effect/platform services.", }, ]); + +export const effectCoreRestrictedImportPatterns = Object.freeze([ + { + group: [ + "../shell/**", + "../../shell/**", + "../../../shell/**", + "./shell/**", + "src/shell/**", + "shell/**", + ], + message: "CORE must not import from SHELL.", + }, +]); diff --git a/packages/effect-ts-check/src/rules/index.mjs b/packages/effect-ts-check/src/rules/index.mjs index 5948465..0be1141 100644 --- a/packages/effect-ts-check/src/rules/index.mjs +++ b/packages/effect-ts-check/src/rules/index.mjs @@ -1,3 +1,13 @@ -export { effectRestrictedImportPatterns, effectRestrictedImports } from "./imports.mjs"; -export { effectStrictSyntaxRestrictions, effectSyntaxRestrictions } from "./syntax.mjs"; +export { + effectCoreRestrictedImportPatterns, + effectRestrictedImportPatterns, + effectRestrictedImports, +} from "./imports.mjs"; +export { + effectCoreSyntaxRestrictions, + effectErrorBoundarySyntaxRestrictions, + effectHostSyntaxRestrictions, + effectStrictSyntaxRestrictions, + effectSyntaxRestrictions, +} from "./syntax.mjs"; export { effectTypeRules } from "./types.mjs"; diff --git a/packages/effect-ts-check/src/rules/syntax.mjs b/packages/effect-ts-check/src/rules/syntax.mjs index eba8f22..fc58443 100644 --- a/packages/effect-ts-check/src/rules/syntax.mjs +++ b/packages/effect-ts-check/src/rules/syntax.mjs @@ -29,7 +29,7 @@ export const effectSyntaxRestrictions = Object.freeze([ }, ]); -export const effectStrictSyntaxRestrictions = Object.freeze([ +export const effectHostSyntaxRestrictions = Object.freeze([ { selector: "CallExpression[callee.name='fetch']", message: "Use @effect/platform HttpClient instead of fetch.", @@ -50,6 +50,9 @@ export const effectStrictSyntaxRestrictions = Object.freeze([ selector: "CallExpression[callee.object.name='global'][callee.property.name='fetch']", message: "Use @effect/platform HttpClient instead of global.fetch.", }, +]); + +export const effectErrorBoundarySyntaxRestrictions = Object.freeze([ { selector: "CallExpression[callee.property.name='catchAll']", message: "Avoid catchAll that swallows typed errors; map or rethrow explicitly.", @@ -62,8 +65,27 @@ export const effectStrictSyntaxRestrictions = Object.freeze([ selector: "TSTypeAssertion", message: "Avoid casts in product code; keep them in one axioms boundary if needed.", }, +]); + +export const effectCoreSyntaxRestrictions = Object.freeze([ { selector: "TSUnknownKeyword", message: "Use unknown only at shell boundaries with decoding.", }, + { + selector: "CallExpression[callee.property.name='runSyncExit']", + message: "Use Effect.runSyncExit only at shell/runtime boundaries.", + }, + { + selector: "CallExpression[callee.property.name='runSync']", + message: "Use Effect.runSync only at shell/runtime boundaries.", + }, + { + selector: "CallExpression[callee.property.name='runPromise']", + message: "Use Effect.runPromise only at shell/runtime boundaries.", + }, +]); + +export const effectStrictSyntaxRestrictions = Object.freeze([ + ...effectHostSyntaxRestrictions, ]); diff --git a/packages/effect-ts-check/src/run.mjs b/packages/effect-ts-check/src/run.mjs index bf2d876..2d887d2 100644 --- a/packages/effect-ts-check/src/run.mjs +++ b/packages/effect-ts-check/src/run.mjs @@ -2,6 +2,7 @@ import { ESLint } from "eslint"; import { minimal } from "./minimal.mjs"; import { strict } from "./strict.mjs"; +import { strictFormat } from "./strict-format.mjs"; export function parseArguments(argv) { const result = { @@ -40,6 +41,10 @@ export function parseArguments(argv) { } export function getProfileConfig(profile) { + if (profile === "strict-format") { + return strictFormat; + } + if (profile === "strict") { return strict; } @@ -52,6 +57,13 @@ export function getProfileConfig(profile) { } function resolveProfileConfig(profile) { + if (profile === "strict-format") { + return { + ok: true, + config: strictFormat, + }; + } + if (profile === "strict") { return { ok: true, @@ -78,10 +90,13 @@ export function printUsage() { "Usage:", " effect-ts-check [paths...]", " effect-ts-check --profile strict [paths...]", + " effect-ts-check --profile strict-format [paths...]", "", "Profiles:", " minimal Default fast effect compliance check.", - " strict Adds import/type/host API policy checks.", + " strict Adds import/type/host API and Effect boundary policy checks.", + " strict-format", + " Runs strict plus the official @effect/dprint formatting preset.", "", ].join("\n"), ); diff --git a/packages/effect-ts-check/src/strict-format.mjs b/packages/effect-ts-check/src/strict-format.mjs new file mode 100644 index 0000000..352da25 --- /dev/null +++ b/packages/effect-ts-check/src/strict-format.mjs @@ -0,0 +1,10 @@ +import * as effectEslint from "@effect/eslint-plugin"; + +import { strict } from "./strict.mjs"; + +export const strictFormat = [ + ...strict, + ...effectEslint.configs.dprint, +]; + +export default strictFormat; diff --git a/packages/effect-ts-check/src/strict.mjs b/packages/effect-ts-check/src/strict.mjs index db79c24..23415e2 100644 --- a/packages/effect-ts-check/src/strict.mjs +++ b/packages/effect-ts-check/src/strict.mjs @@ -1,8 +1,16 @@ -import * as effectEslint from "@effect/eslint-plugin"; +import eslintComments from "@eslint-community/eslint-plugin-eslint-comments"; import tseslint from "typescript-eslint"; -import { effectBaseConfig, effectFileGlobs } from "./base.mjs"; import { + effectCoreAxiomsFileGlobs, + effectCoreFileGlobs, + effectFileGlobs, + effectIgnoreConfig, +} from "./base.mjs"; +import { + effectCoreRestrictedImportPatterns, + effectCoreSyntaxRestrictions, + effectErrorBoundarySyntaxRestrictions, effectRestrictedImportPatterns, effectRestrictedImports, effectStrictSyntaxRestrictions, @@ -10,9 +18,33 @@ import { effectTypeRules, } from "./rules/index.mjs"; +const strictSyntaxRestrictions = Object.freeze([ + ...effectSyntaxRestrictions, + ...effectStrictSyntaxRestrictions, +]); + +const coreSyntaxRestrictions = Object.freeze([ + ...strictSyntaxRestrictions, + ...effectErrorBoundarySyntaxRestrictions, + ...effectCoreSyntaxRestrictions, +]); + +const coreAxiomsSyntaxRestrictions = Object.freeze([ + ...strictSyntaxRestrictions, + ...effectErrorBoundarySyntaxRestrictions.filter((rule) => + rule.selector !== "TSAsExpression" && rule.selector !== "TSTypeAssertion" + ), + ...effectCoreSyntaxRestrictions, +]); + +const strictImportPatterns = Object.freeze([...effectRestrictedImportPatterns]); +const coreImportPatterns = Object.freeze([ + ...effectRestrictedImportPatterns, + ...effectCoreRestrictedImportPatterns, +]); + export const strict = [ - ...effectBaseConfig, - ...effectEslint.configs.dprint, + effectIgnoreConfig, { name: "effect-ts-check/strict", files: effectFileGlobs, @@ -23,22 +55,45 @@ export const strict = [ }, plugins: { "@typescript-eslint": tseslint.plugin, + "eslint-comments": eslintComments, }, rules: { "no-console": "error", + "no-throw-literal": "error", "no-restricted-imports": [ "error", { paths: effectRestrictedImports, - patterns: effectRestrictedImportPatterns, + patterns: strictImportPatterns, }, ], - "no-restricted-syntax": [ + "no-restricted-syntax": ["error", ...strictSyntaxRestrictions], + "eslint-comments/no-use": "error", + "eslint-comments/no-unlimited-disable": "error", + "eslint-comments/disable-enable-pair": "error", + "eslint-comments/no-unused-disable": "error", + ...effectTypeRules, + }, + }, + { + name: "effect-ts-check/strict-core", + files: effectCoreFileGlobs, + rules: { + "no-restricted-imports": [ "error", - ...effectSyntaxRestrictions, - ...effectStrictSyntaxRestrictions, + { + paths: effectRestrictedImports, + patterns: coreImportPatterns, + }, ], - ...effectTypeRules, + "no-restricted-syntax": ["error", ...coreSyntaxRestrictions], + }, + }, + { + name: "effect-ts-check/strict-core-axioms", + files: effectCoreAxiomsFileGlobs, + rules: { + "no-restricted-syntax": ["error", ...coreAxiomsSyntaxRestrictions], }, }, ]; diff --git a/packages/effect-ts-check/tests/cli.test.mjs b/packages/effect-ts-check/tests/cli.test.mjs index de90233..5cffd81 100644 --- a/packages/effect-ts-check/tests/cli.test.mjs +++ b/packages/effect-ts-check/tests/cli.test.mjs @@ -80,6 +80,23 @@ test("cli supports strict profile", () => { assert.match(result.stdout, /no-restricted-imports|@typescript-eslint\/no-explicit-any|no-console/) }) +test("cli supports strict-format profile", () => { + const fixture = writeTempFixture("pass.ts", "const value = 1;\nexport { value };\n") + const result = spawnSync( + process.execPath, + [cliPath, "--profile", "strict-format", fixture.path], + { + cwd: packageDir, + encoding: "utf8" + } + ) + fixture.cleanup() + + assert.equal(result.status, 0) + assert.equal(result.stdout, "") + assert.equal(result.stderr, "") +}) + test("cli strict profile keeps minimal syntax checks", () => { const fixture = writeTempFixture( "fail.ts", diff --git a/packages/effect-ts-check/tests/configs.test.mjs b/packages/effect-ts-check/tests/configs.test.mjs index 7110540..6ce8457 100644 --- a/packages/effect-ts-check/tests/configs.test.mjs +++ b/packages/effect-ts-check/tests/configs.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict" import test from "node:test" -import { minimal, strict } from "../src/index.mjs" +import { minimal, strict, strictFormat } from "../src/index.mjs" import { lintSnippet } from "./helpers.mjs" test("minimal exports a flat config array", () => { @@ -49,10 +49,24 @@ test("strict retains minimal syntax policy", async () => { assert.ok(result.messages.some((message) => message.ruleId === "no-restricted-syntax")) }) -test("strict includes additional effect-eslint preset layers", () => { +test("strict adds additional compliance layers", () => { assert.ok(strict.length > minimal.length) }) +test("strict-format adds the effect dprint preset separately", () => { + assert.ok(strictFormat.length > strict.length) + assert.ok( + strictFormat.some((entry) => + entry.rules && Object.hasOwn(entry.rules, "@effect/dprint") + ) + ) + assert.ok( + strict.every((entry) => + !entry.rules || !Object.hasOwn(entry.rules, "@effect/dprint") + ) + ) +}) + test("minimal leaves import and type policy to strict", async () => { const [result] = await lintSnippet( minimal, @@ -95,6 +109,108 @@ test("strict applies to common module extensions", async () => { } }) +test("strict rejects eslint disable comments", async () => { + const [result] = await lintSnippet( + strict, + "/* eslint-disable no-console */\nconsole.log(1)\n", + "demo.ts" + ) + + const ruleIds = result.messages.map((message) => message.ruleId) + + assert.ok(ruleIds.includes("eslint-comments/no-use")) +}) + +test("strict rejects direct fetch", async () => { + const [result] = await lintSnippet( + strict, + "fetch('https://example.com')\n", + "demo.ts" + ) + + assert.ok(result.messages.some((message) => message.ruleId === "no-restricted-syntax")) +}) + +test("strict rejects thrown literals without typed linting", async () => { + const [result] = await lintSnippet( + strict, + "throw 'BadError: failed'\n", + "demo.ts" + ) + + assert.ok(result.messages.some((message) => message.ruleId === "no-throw-literal")) +}) + +test("strict keeps runtime execution at shell boundaries", async () => { + const coreResult = await lintSnippet( + strict, + "Effect.runPromise(Effect.succeed(1))\n", + "src/core/program.ts" + ) + const shellResult = await lintSnippet( + strict, + "Effect.runPromise(Effect.succeed(1))\n", + "src/shell/program.ts" + ) + + assert.ok( + coreResult[0].messages.some((message) => message.ruleId === "no-restricted-syntax") + ) + assert.ok( + shellResult[0].messages.every((message) => message.ruleId !== "no-restricted-syntax") + ) +}) + +test("strict blocks core imports from shell", async () => { + const [result] = await lintSnippet( + strict, + 'import { service } from "../shell/service"\nexport { service }\n', + "src/core/usecase.ts" + ) + + assert.ok(result.messages.some((message) => message.ruleId === "no-restricted-imports")) +}) + +test("strict keeps casts inside the axioms boundary", async () => { + const coreResult = await lintSnippet( + strict, + "const value = input as string\n", + "src/core/usecase.ts" + ) + const axiomsResult = await lintSnippet( + strict, + "const value = input as string\n", + "src/core/axioms.ts" + ) + + assert.ok( + coreResult[0].messages.some((message) => message.ruleId === "no-restricted-syntax") + ) + assert.ok( + axiomsResult[0].messages.every((message) => message.ruleId !== "no-restricted-syntax") + ) +}) + +test("strict keeps catchAll out of core while allowing outer handlers", async () => { + const coreResult = await lintSnippet( + strict, + "const handler = Effect.catchAll(() => Effect.succeed(1))\n", + "src/core/usecase.ts" + ) + const apiResult = await lintSnippet( + strict, + "const handler = Effect.catchAll(() => Effect.succeed(1))\n", + "src/api/http.ts" + ) + + assert.ok( + coreResult[0].messages.some((message) => message.ruleId === "no-restricted-syntax") + ) + assert.ok( + apiResult[0].messages.every((message) => message.ruleId !== "no-restricted-syntax") + ) +}) + test("minimal ignores nested tests and fixtures", async () => { const [testResult] = await lintSnippet( minimal, diff --git a/plugins/effect-ts-skills/skills/effect-ts-guide/SKILL.md b/plugins/effect-ts-skills/skills/effect-ts-guide/SKILL.md index 7df5087..1da986d 100644 --- a/plugins/effect-ts-skills/skills/effect-ts-guide/SKILL.md +++ b/plugins/effect-ts-skills/skills/effect-ts-guide/SKILL.md @@ -23,9 +23,15 @@ If the repository is mostly tooling, docs, or test fixtures for the checker itse bash "$SKILL_DIR/scripts/run-effect-ts-check.sh" --profile strict ``` -4. Fix the violations that are machine-detectable. -5. Apply the manual rules from the references for architecture and style decisions. -6. Re-run the relevant check before finishing. +4. If the repository wants the official Effect dprint formatting gate too, run: + +```bash +bash "$SKILL_DIR/scripts/run-effect-ts-check.sh" --profile strict-format +``` + +5. Fix the violations that are machine-detectable. +6. Apply the manual rules from the references for architecture and style decisions. +7. Re-run the relevant check before finishing. For editor integration tasks, treat `effect-ts-check` as the reusable CLI/compliance package and `@effect/language-service` plus VSCode settings/extensions as a separate setup concern. @@ -42,7 +48,9 @@ The compliance check is for fast, repeatable signals: - `require` - common JavaScript and TypeScript module extensions, including `.jsx`, `.mts`, and `.cts` -The `strict` profile also layers in unsafe host import checks, obvious typing-policy violations such as `any` and `ts-ignore`, unsupported casts, direct `fetch`, `catchAll`, and the official `@effect/eslint-plugin` preset for Effect-aware lint behavior. +The `strict` profile also layers in unsafe host import checks, obvious typing-policy violations such as `any` and `ts-ignore`, direct `fetch`, eslint-disable hygiene, thrown-literal checks, and path-aware CORE boundary rules for casts, `unknown`, `catchAll`, CORE-to-SHELL imports, and runtime execution calls such as `Effect.runPromise` / `Effect.runSync`. + +The `strict-format` profile runs `strict` plus the official `@effect/dprint` formatting preset. Use it when formatting should be part of the compliance gate; use `strict` when you need semantic policy results without formatting noise. The wrapper uses `npx` with a bundled `effect-ts-check` tarball. It can run from a standalone skill install, but npm registry access or an npm cache is required for package dependencies. @@ -56,7 +64,7 @@ The check does not replace architectural reasoning. Apply manual rules for: - boundary decoding with `@effect/schema` - resource safety with `Effect.acquireRelease` and `Effect.scoped` - exhaustive handling of unions -- whether `Effect.runPromise`, `Effect.runSync`, and similar runtime execution calls are isolated to shell entrypoints +- project-specific service factories, local package boundaries, and allowed runtime entrypoint layouts outside the default CORE/SHELL convention ## References diff --git a/plugins/effect-ts-skills/skills/effect-ts-guide/assets/effect-ts-check/prover-coder-ai-effect-ts-check-0.1.0.tgz b/plugins/effect-ts-skills/skills/effect-ts-guide/assets/effect-ts-check/prover-coder-ai-effect-ts-check-0.1.0.tgz index fd06654e0f8bca7220d836b309d629b85bc2ed27..b5fe87890cfe016ac605fa56894c5e35fc00bc47 100644 GIT binary patch literal 3940 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 literal 3371 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 BwSWKs diff --git a/plugins/effect-ts-skills/skills/effect-ts-guide/references/lint-checks.md b/plugins/effect-ts-skills/skills/effect-ts-guide/references/lint-checks.md index e686a36..2368cdb 100644 --- a/plugins/effect-ts-skills/skills/effect-ts-guide/references/lint-checks.md +++ b/plugins/effect-ts-skills/skills/effect-ts-guide/references/lint-checks.md @@ -30,14 +30,31 @@ The minimal profile should catch the high-signal Effect violations: The strict profile should add deeper policy checks: -- the official `@effect/eslint-plugin` preset - direct host imports that bypass Effect platform services - obvious unsafe typing policy violations - safer handling around casts and `unknown` - direct `fetch` and other host API restrictions - eslint comment hygiene +- thrown-literal checks +- path-aware CORE/SHELL boundary checks -Runtime execution boundaries such as `Effect.runPromise` and CORE/SHELL import direction still need manual review because the correct answer depends on the repository's entrypoint layout. +The CORE/SHELL checks apply to `src/core/**` by default: + +- CORE must not import from SHELL. +- `Effect.runPromise`, `Effect.runSync`, and `Effect.runSyncExit` are shell/runtime-boundary only. +- `catchAll`, casts, and `unknown` are blocked in CORE, except casts are allowed in `src/core/axioms.ts`. + +## Strict Format Profile + +Use `--profile strict-format` when formatting should be part of the gate: + +```bash +bash "$SKILL_DIR/scripts/run-effect-ts-check.sh" --profile strict-format +``` + +This runs `strict` plus the official `@effect/dprint` preset. Keep it separate from `strict` when you need actionable semantic findings without formatting noise. + +Runtime execution boundaries outside the default `src/core/**` / `src/shell/**` convention still need manual review because the correct answer depends on the repository's entrypoint layout. ## Editor Tooling Boundary diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4436f43..3b25589 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ importers: '@effect/eslint-plugin': specifier: ^0.3.2 version: 0.3.2 + '@eslint-community/eslint-plugin-eslint-comments': + specifier: ^4.7.2 + version: 4.7.2(eslint@10.2.0) '@eslint/js': specifier: ^10.0.0 version: 10.0.1(eslint@10.2.0) @@ -34,6 +37,12 @@ packages: '@effect/eslint-plugin@0.3.2': resolution: {integrity: sha512-c4Vs9t3r54A4Zpl+wo8+PGzZz3JWYsip41H+UrebRLjQ2Hk/ap63IeCgN/HWcYtxtyhRopjp7gW9nOQ2Snbl+g==} + '@eslint-community/eslint-plugin-eslint-comments@4.7.2': + resolution: {integrity: sha512-LF03qURSwEWm2dz5wtdDCzNk+7Opl0X7q6I3undsaIuNsEiNvRV3BCtqu14Q/6Pzg1tBj44LcxpW2EpSLZStZw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -436,6 +445,12 @@ snapshots: '@dprint/typescript': 0.91.8 prettier-linter-helpers: 1.0.1 + '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.2.0)': + dependencies: + escape-string-regexp: 4.0.0 + eslint: 10.2.0 + ignore: 7.0.5 + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0)': dependencies: eslint: 10.2.0 diff --git a/tools/validate-distribution.mjs b/tools/validate-distribution.mjs index 040b724..cb9d41b 100644 --- a/tools/validate-distribution.mjs +++ b/tools/validate-distribution.mjs @@ -185,18 +185,27 @@ function assertLintCheckDocs() { "`unknown`", "`fetch`", "host API restrictions", + "CORE/SHELL", ]) { if (!strictSection.includes(expected)) { fail(`Strict profile docs must mention ${expected}`) } } + if (!contents.includes("## Strict Format Profile")) { + fail("Lint docs must document strict-format profile") + } + + if (!contents.includes("@effect/dprint")) { + fail("Lint docs must mention strict-format dprint behavior") + } + if (!contents.includes("Runtime execution boundaries")) { fail("Lint docs must call runtime execution boundaries manual review") } - if (!contents.includes("CORE/SHELL import direction")) { - fail("Lint docs must call CORE/SHELL import direction manual review") + if (!contents.includes("CORE must not import from SHELL")) { + fail("Lint docs must document machine-checked CORE/SHELL import direction") } } From bea8a9a228ce79b81957ce226cbd1b61bd423c18 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:18:19 +0000 Subject: [PATCH 2/4] Use ProverCoderAI marketplace alias --- .agents/plugins/marketplace.json | 4 ++-- README.md | 4 ++-- tools/validate-distribution.mjs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index dd27e20..7db121c 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -1,7 +1,7 @@ { - "name": "effect-ts-skills", + "name": "ProverCoderAI", "interface": { - "displayName": "Effect TS Skills" + "displayName": "ProverCoderAI" }, "plugins": [ { diff --git a/README.md b/README.md index 5bfe9be..1802b49 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ The `effect-ts-guide` skill is intentionally kept inside the plugin. There is no ```bash codex plugin marketplace add ProverCoderAI/effect-ts-skills -codex plugin add effect-ts-skills@effect-ts-skills +codex plugin add ProverCoderAI@effect-ts-skills ``` For local development from a checkout: ```bash codex plugin marketplace add . -codex plugin add effect-ts-skills@effect-ts-skills +codex plugin add ProverCoderAI@effect-ts-skills ``` The repo marketplace entry points at `plugins/effect-ts-skills`, which is the canonical plugin directory. diff --git a/tools/validate-distribution.mjs b/tools/validate-distribution.mjs index cb9d41b..6a50705 100644 --- a/tools/validate-distribution.mjs +++ b/tools/validate-distribution.mjs @@ -91,7 +91,7 @@ function assertMarketplace() { assertPathExists(marketplacePath, "Repo plugin marketplace") const marketplace = readJson(marketplacePath) - if (marketplace.name !== "effect-ts-skills") { + if (marketplace.name !== "ProverCoderAI") { fail(`Unexpected marketplace name: ${marketplace.name}`) } From c612bb4bcf1491399a34403d1981e574b281f015 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:22:34 +0000 Subject: [PATCH 3/4] Fix plugin install selector docs --- .agents/plugins/marketplace.json | 4 ++-- README.md | 4 ++-- tools/validate-distribution.mjs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 7db121c..dd27e20 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -1,7 +1,7 @@ { - "name": "ProverCoderAI", + "name": "effect-ts-skills", "interface": { - "displayName": "ProverCoderAI" + "displayName": "Effect TS Skills" }, "plugins": [ { diff --git a/README.md b/README.md index 1802b49..5bfe9be 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ The `effect-ts-guide` skill is intentionally kept inside the plugin. There is no ```bash codex plugin marketplace add ProverCoderAI/effect-ts-skills -codex plugin add ProverCoderAI@effect-ts-skills +codex plugin add effect-ts-skills@effect-ts-skills ``` For local development from a checkout: ```bash codex plugin marketplace add . -codex plugin add ProverCoderAI@effect-ts-skills +codex plugin add effect-ts-skills@effect-ts-skills ``` The repo marketplace entry points at `plugins/effect-ts-skills`, which is the canonical plugin directory. diff --git a/tools/validate-distribution.mjs b/tools/validate-distribution.mjs index 6a50705..cb9d41b 100644 --- a/tools/validate-distribution.mjs +++ b/tools/validate-distribution.mjs @@ -91,7 +91,7 @@ function assertMarketplace() { assertPathExists(marketplacePath, "Repo plugin marketplace") const marketplace = readJson(marketplacePath) - if (marketplace.name !== "ProverCoderAI") { + if (marketplace.name !== "effect-ts-skills") { fail(`Unexpected marketplace name: ${marketplace.name}`) } From 72e52474e8707d68660038d2ab546fa8c122d7a7 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:26:56 +0000 Subject: [PATCH 4/4] Simplify README --- README.md | 42 +++++------------------------------------- 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 5bfe9be..4544e81 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,21 @@ # effect-ts-skills -Reusable Effect-TS skills and compliance tooling for [Codex](https://github.com/openai/codex). +Codex plugin with an Effect-TS guide skill and a bundled `effect-ts-check` CLI. -## Plugin - -This repository publishes one Codex plugin: - -| Plugin | Path | Bundled skill | -|--------|------|---------------| -| `effect-ts-skills` | `plugins/effect-ts-skills` | `effect-ts-guide` | - -The `effect-ts-guide` skill is intentionally kept inside the plugin. There is no separate root-level standalone skill copy. - -## Installation +## Install ```bash codex plugin marketplace add ProverCoderAI/effect-ts-skills codex plugin add effect-ts-skills@effect-ts-skills ``` -For local development from a checkout: - -```bash -codex plugin marketplace add . -codex plugin add effect-ts-skills@effect-ts-skills -``` - -The repo marketplace entry points at `plugins/effect-ts-skills`, which is the canonical plugin directory. - -After installation, start a new Codex thread and invoke the skill explicitly with `$effect-ts-guide` or ask for an Effect-TS implementation/review task. +## Use -## Development +Start a new Codex thread and ask for `$effect-ts-guide`, or ask Codex to review/fix Effect-TS code. -This repository is a [pnpm workspace](https://pnpm.io/workspaces). +## Develop ```bash corepack pnpm install -corepack pnpm run sync:distribution corepack pnpm run check ``` - -### Structure - -``` -plugins/effect-ts-skills/ # Installable Codex plugin with bundled effect-ts-guide skill -packages/effect-ts-check/ # Reusable Effect-TS compliance CLI -tools/ # Repo-level validation scripts -``` - -## License - -ISC