From 4004c19b8eac0bda453f66addf25b12bcc610c87 Mon Sep 17 00:00:00 2001 From: Fahmi Harun <34875577+kuker24@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:57:30 +0700 Subject: [PATCH 1/4] feat(anti-slop): sync assets to c44ef22 --- .../assets/anti-slop/effect/index.ts | 8 + .../rules/no-manual-effect-error-tag.ts | 52 + .../effect/rules/no-manual-tag-comparison.ts | 45 + .../rules/no-manual-tagged-construction.ts | 37 + .../effect/rules/prefer-effect-match.ts | 54 ++ .../anti-slop/effect/shared/tagged-values.ts | 97 ++ .../assets/anti-slop/index.ts | 6 + .../anti-slop/rules/no-array-filter-map.ts | 28 + .../rules/no-known-value-widening.ts | 16 +- .../anti-slop/rules/no-module-mocking.ts | 17 +- .../rules/no-reduce-accumulator-copy.ts | 109 +++ .../rules/require-readable-spacing.ts | 47 + .../assets/anti-slop/shared/array-method.ts | 94 ++ .../assets/anti-slop/shared/reflect-method.ts | 15 +- .../assets/anti-slop/shared/scope.ts | 15 + .../anti-slop/vendor/eslint-stylistic/LICENSE | 22 + .../vendor/eslint-stylistic/UPSTREAM.md | 28 + .../eslint-stylistic/padding-line-ast.ts | 51 + .../padding-line-between-statements.ts | 906 ++++++++++++++++++ .../padding-line-options.d.ts | 87 ++ .../fixtures/anti-slop-c44ef22-manifest.json | 40 + 21 files changed, 1733 insertions(+), 41 deletions(-) create mode 100644 skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-effect-error-tag.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tag-comparison.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tagged-construction.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/effect/rules/prefer-effect-match.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/effect/shared/tagged-values.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/rules/no-array-filter-map.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/rules/no-reduce-accumulator-copy.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/rules/require-readable-spacing.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/shared/array-method.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/shared/scope.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/LICENSE create mode 100644 skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md create mode 100644 skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts create mode 100644 skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts create mode 100644 tests/fixtures/anti-slop-c44ef22-manifest.json diff --git a/skills/install-anti-slop/assets/anti-slop/effect/index.ts b/skills/install-anti-slop/assets/anti-slop/effect/index.ts index 3724786..55eb13d 100644 --- a/skills/install-anti-slop/assets/anti-slop/effect/index.ts +++ b/skills/install-anti-slop/assets/anti-slop/effect/index.ts @@ -1,12 +1,20 @@ import { eslintCompatPlugin } from "@oxlint/plugins"; +import { noManualEffectErrorTagRule } from "./rules/no-manual-effect-error-tag.ts"; +import { noManualTagComparisonRule } from "./rules/no-manual-tag-comparison.ts"; +import { noManualTaggedConstructionRule } from "./rules/no-manual-tagged-construction.ts"; import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts"; +import { preferEffectMatchRule } from "./rules/prefer-effect-match.ts"; /** Opt-in Oxlint rules for Effect service and Layer architecture. */ const antiSlopEffectPlugin = eslintCompatPlugin({ meta: { name: "anti-slop-effect" }, rules: { + "no-manual-effect-error-tag": noManualEffectErrorTagRule, + "no-manual-tag-comparison": noManualTagComparisonRule, + "no-manual-tagged-construction": noManualTaggedConstructionRule, "no-service-constructor-imports": noServiceConstructorImportsRule, + "prefer-effect-match": preferEffectMatchRule, }, }); diff --git a/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-effect-error-tag.ts b/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-effect-error-tag.ts new file mode 100644 index 0000000..daaa109 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-effect-error-tag.ts @@ -0,0 +1,52 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isInsideBroadEffectHandler, + isReasonTagMember, + isTagMember, + tagMemberFromComparison, +} from "../shared/tagged-values.ts"; + +export const noManualEffectErrorTagRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Effect tagged error handlers instead of manually branching on `_tag` in a catch handler.", + }, + messages: { + tag: "Use Effect.catchTag or Effect.catchTags instead of manually discriminating a tagged error in a broad Effect catch handler.", + reason: + "Use Effect.catchReason or Effect.catchReasons instead of manually discriminating a tagged `reason` in a broad Effect catch handler.", + }, + }, + createOnce(context) { + return { + BinaryExpression(node) { + const tagMember = tagMemberFromComparison(node); + if ( + tagMember === undefined || + !isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ + node, + messageId: isReasonTagMember(tagMember) ? "reason" : "tag", + }); + }, + SwitchStatement(node) { + if ( + !isTagMember(node.discriminant) || + !isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ + node, + messageId: isReasonTagMember(node.discriminant) ? "reason" : "tag", + }); + }, + }; + }, +}); diff --git a/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tag-comparison.ts b/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tag-comparison.ts new file mode 100644 index 0000000..868d1d2 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tag-comparison.ts @@ -0,0 +1,45 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isInsideBroadEffectHandler, + isTagMember, + tagMemberFromComparison, +} from "../shared/tagged-values.ts"; + +export const noManualTagComparisonRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Effect Match or Predicate helpers instead of manually branching on `_tag`.", + }, + messages: { + manualComparison: + "Use Match.tag/Match.tags for tagged-value branching, or Predicate.isTagged for a simple reusable predicate.", + manualSwitch: + "Use Match.value(value).pipe(Match.tag/Match.tags/Match.tagsExhaustive) or the tagged enum `$match` helper instead of switching on `_tag`.", + }, + }, + createOnce(context) { + return { + BinaryExpression(node) { + if ( + tagMemberFromComparison(node) === undefined || + isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ node, messageId: "manualComparison" }); + }, + SwitchStatement(node) { + if ( + !isTagMember(node.discriminant) || + isInsideBroadEffectHandler(node) + ) { + return; + } + context.report({ node, messageId: "manualSwitch" }); + }, + }; + }, +}); diff --git a/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tagged-construction.ts b/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tagged-construction.ts new file mode 100644 index 0000000..382f166 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/effect/rules/no-manual-tagged-construction.ts @@ -0,0 +1,37 @@ +import { defineRule } from "@oxlint/plugins"; + +import { + isMatchPatternObject, + isStringLiteral, + propertyName, +} from "../shared/tagged-values.ts"; + +export const noManualTaggedConstructionRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Construct tagged values with their existing Effect constructor instead of writing `_tag` manually.", + }, + messages: { + manualConstruction: + "Use the existing Schema tagged `.make`, tagged class/error constructor, or Data.taggedEnum variant constructor instead of writing a literal `_tag` object.", + }, + }, + createOnce(context) { + return { + ObjectExpression(node) { + if (isMatchPatternObject(node)) return; + const tag = node.properties.find( + (property) => + property.type === "Property" && + propertyName(property) === "_tag" && + isStringLiteral(property.value), + ); + if (tag !== undefined) { + context.report({ node: tag, messageId: "manualConstruction" }); + } + }, + }; + }, +}); diff --git a/skills/install-anti-slop/assets/anti-slop/effect/rules/prefer-effect-match.ts b/skills/install-anti-slop/assets/anti-slop/effect/rules/prefer-effect-match.ts new file mode 100644 index 0000000..d834540 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/effect/rules/prefer-effect-match.ts @@ -0,0 +1,54 @@ +import { defineRule, type ESTree } from "@oxlint/plugins"; + +const equalityOperators = new Set(["==", "===", "!=", "!=="]); + +export const preferEffectMatchRule = defineRule({ + meta: { + type: "problem", + docs: { + description: + "Use Match from Effect for chained literal ternaries over the same value.", + }, + messages: { + preferMatch: + "Use Match from Effect instead of a chained literal ternary.", + }, + }, + createOnce(context) { + const isLiteral = (node: ESTree.Node): boolean => + node.type === "Literal" || + (node.type === "TemplateLiteral" && node.expressions.length === 0); + + const comparedValue = (node: ESTree.Expression): string | undefined => { + if ( + node.type !== "BinaryExpression" || + !equalityOperators.has(node.operator) + ) { + return undefined; + } + if (isLiteral(node.left)) return context.sourceCode.getText(node.right); + if (isLiteral(node.right)) return context.sourceCode.getText(node.left); + return undefined; + }; + + return { + ConditionalExpression(node) { + if (node.parent?.type === "ConditionalExpression") return; + const value = comparedValue(node.test); + if (value === undefined) return; + + let alternate = node.alternate; + let literalChecks = 1; + while (alternate.type === "ConditionalExpression") { + if (comparedValue(alternate.test) !== value) return; + literalChecks += 1; + alternate = alternate.alternate; + } + + if (literalChecks > 1) { + context.report({ node, messageId: "preferMatch" }); + } + }, + }; + }, +}); diff --git a/skills/install-anti-slop/assets/anti-slop/effect/shared/tagged-values.ts b/skills/install-anti-slop/assets/anti-slop/effect/shared/tagged-values.ts new file mode 100644 index 0000000..21d10a2 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/effect/shared/tagged-values.ts @@ -0,0 +1,97 @@ +import type { ESTree } from "@oxlint/plugins"; + +const equalityOperators = new Set(["==", "===", "!=", "!=="]); +const broadEffectCatchMethods = new Set(["catch", "catchAll", "catchIf"]); + +export const isStringLiteral = ( + node: ESTree.Node | null | undefined, +): node is ESTree.StringLiteral => + node?.type === "Literal" && typeof node.value === "string"; + +export const isTagMember = ( + node: ESTree.Node | null | undefined, +): node is ESTree.MemberExpression => + node?.type === "MemberExpression" && + ((!node.computed && + node.property.type === "Identifier" && + node.property.name === "_tag") || + (node.computed && + isStringLiteral(node.property) && + node.property.value === "_tag")); + +export const tagMemberFromComparison = ( + node: ESTree.BinaryExpression, +): ESTree.MemberExpression | undefined => { + if (!equalityOperators.has(node.operator)) return undefined; + if (isTagMember(node.left) && isStringLiteral(node.right)) return node.left; + if (isTagMember(node.right) && isStringLiteral(node.left)) return node.right; + return undefined; +}; + +const isBroadEffectCatchCall = ( + node: ESTree.Node | null | undefined, +): node is ESTree.CallExpression => + node?.type === "CallExpression" && + node.callee.type === "MemberExpression" && + node.callee.object.type === "Identifier" && + node.callee.object.name === "Effect" && + !node.callee.computed && + node.callee.property.type === "Identifier" && + broadEffectCatchMethods.has(node.callee.property.name); + +export const isInsideBroadEffectHandler = (node: ESTree.Node): boolean => { + let current: ESTree.Node | null | undefined = node.parent; + while (current !== null && current !== undefined) { + if ( + current.type === "ArrowFunctionExpression" || + current.type === "FunctionExpression" + ) { + return ( + isBroadEffectCatchCall(current.parent) && + current.parent.arguments.includes(current) + ); + } + current = current.parent; + } + return false; +}; + +export const isReasonTagMember = (node: ESTree.MemberExpression): boolean => + node.object.type === "MemberExpression" && + ((!node.object.computed && + node.object.property.type === "Identifier" && + node.object.property.name === "reason") || + (node.object.computed && + isStringLiteral(node.object.property) && + node.object.property.value === "reason")); + +export const propertyName = ( + property: ESTree.ObjectProperty, +): string | undefined => { + if (!property.computed && property.key.type === "Identifier") { + return property.key.name; + } + if ( + property.key.type === "Literal" && + typeof property.key.value === "string" + ) { + return property.key.value; + } + return undefined; +}; + +export const isMatchPatternObject = (node: ESTree.ObjectExpression): boolean => { + const call = node.parent; + if (call?.type !== "CallExpression" || !call.arguments.includes(node)) { + return false; + } + const callee = call.callee; + return ( + callee.type === "MemberExpression" && + callee.object.type === "Identifier" && + callee.object.name === "Match" && + !callee.computed && + callee.property.type === "Identifier" && + (callee.property.name === "when" || callee.property.name === "not") + ); +}; diff --git a/skills/install-anti-slop/assets/anti-slop/index.ts b/skills/install-anti-slop/assets/anti-slop/index.ts index 2b4ae22..a3876ec 100644 --- a/skills/install-anti-slop/assets/anti-slop/index.ts +++ b/skills/install-anti-slop/assets/anti-slop/index.ts @@ -1,5 +1,7 @@ import { eslintCompatPlugin } from "@oxlint/plugins"; +import { noArrayFilterMapRule } from "./rules/no-array-filter-map.ts"; +import { noReduceAccumulatorCopyRule } from "./rules/no-reduce-accumulator-copy.ts"; import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts"; import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts"; import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts"; @@ -14,12 +16,15 @@ import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts"; import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts"; import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts"; import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts"; +import { requireReadableSpacingRule } from "./rules/require-readable-spacing.ts"; import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts"; /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */ const antiSlopPlugin = eslintCompatPlugin({ meta: { name: "anti-slop" }, rules: { + "no-array-filter-map": noArrayFilterMapRule, + "no-reduce-accumulator-copy": noReduceAccumulatorCopyRule, "no-chained-type-assertions": noChainedTypeAssertionsRule, "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule, "no-known-value-widening": noKnownValueWideningRule, @@ -34,6 +39,7 @@ const antiSlopPlugin = eslintCompatPlugin({ "no-unknown-returns": noUnknownReturnsRule, "no-unknown-type-aliases": noUnknownTypeAliasesRule, "no-widen-then-assert": noWidenThenAssertRule, + "require-readable-spacing": requireReadableSpacingRule, "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule, }, }); diff --git a/skills/install-anti-slop/assets/anti-slop/rules/no-array-filter-map.ts b/skills/install-anti-slop/assets/anti-slop/rules/no-array-filter-map.ts new file mode 100644 index 0000000..dc001b3 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/rules/no-array-filter-map.ts @@ -0,0 +1,28 @@ +import { defineRule } from "@oxlint/plugins"; + +import { arrayMethodTarget, isKnownArrayExpression, unwrapArrayExpression } from "../shared/array-method.ts"; + +/** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */ +export const noArrayFilterMapRule = defineRule({ + meta: { + type: "suggestion", + docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." }, + messages: { + arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + const outer = arrayMethodTarget(node.callee); + if (outer === null || (outer.name !== "map" && outer.name !== "filter")) return; + const innerCall = unwrapArrayExpression(outer.object); + if (innerCall.type !== "CallExpression") return; + const inner = arrayMethodTarget(innerCall.callee); + if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map")) return; + if (!isKnownArrayExpression(context.sourceCode, inner.object)) return; + context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } }); + }, + }; + }, +}); diff --git a/skills/install-anti-slop/assets/anti-slop/rules/no-known-value-widening.ts b/skills/install-anti-slop/assets/anti-slop/rules/no-known-value-widening.ts index 7defff3..0df39ab 100644 --- a/skills/install-anti-slop/assets/anti-slop/rules/no-known-value-widening.ts +++ b/skills/install-anti-slop/assets/anti-slop/rules/no-known-value-widening.ts @@ -13,8 +13,9 @@ import { functionParameterBindingName, functionParameterTypeAnnotation, } from "../shared/function-parameters.ts"; +import { resolveVariable } from "../shared/scope.ts"; -import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; +import type { ESTree, SourceCode, Variable } from "@oxlint/plugins"; type FunctionExpression = ESTree.ArrowFunctionExpression | ESTree.Function; @@ -32,19 +33,6 @@ function unwrapExpression(expression: ESTree.Expression): ESTree.Expression { return current; } -function resolveVariable( - sourceCode: SourceCode, - identifier: ESTree.IdentifierReference, -): Variable | null { - let scope: Scope | null = sourceCode.getScope(identifier); - while (scope !== null) { - const variable = scope.set.get(identifier.name); - if (variable !== undefined) return variable; - scope = scope.upper; - } - return null; -} - function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null { if (variable.defs.length !== 1) return null; const [definition] = variable.defs; diff --git a/skills/install-anti-slop/assets/anti-slop/rules/no-module-mocking.ts b/skills/install-anti-slop/assets/anti-slop/rules/no-module-mocking.ts index d6fb5b4..2d60d48 100644 --- a/skills/install-anti-slop/assets/anti-slop/rules/no-module-mocking.ts +++ b/skills/install-anti-slop/assets/anti-slop/rules/no-module-mocking.ts @@ -1,21 +1,10 @@ import { defineRule } from "@oxlint/plugins"; -import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; +import { resolveVariable } from "../shared/scope.ts"; -const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]); +import type { ESTree, SourceCode } from "@oxlint/plugins"; -function resolveVariable( - sourceCode: SourceCode, - identifier: ESTree.IdentifierReference, -): Variable | null { - let scope: Scope | null = sourceCode.getScope(identifier); - while (scope !== null) { - const variable = scope.set.get(identifier.name); - if (variable !== undefined) return variable; - scope = scope.upper; - } - return null; -} +const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]); function importedName(node: ESTree.Node): string | null { if (node.type !== "ImportSpecifier") return null; diff --git a/skills/install-anti-slop/assets/anti-slop/rules/no-reduce-accumulator-copy.ts b/skills/install-anti-slop/assets/anti-slop/rules/no-reduce-accumulator-copy.ts new file mode 100644 index 0000000..47d8e2c --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/rules/no-reduce-accumulator-copy.ts @@ -0,0 +1,109 @@ +import { defineRule } from "@oxlint/plugins"; +import type { ESTree, SourceCode, Variable } from "@oxlint/plugins"; + +import { + arrayMethodTarget, + isKnownArrayExpression, + resolveArrayBinding, + unwrapArrayExpression, +} from "../shared/array-method.ts"; + +function enclosingReducer(node: ESTree.Node) { + let parent = node.parent; + while (parent !== null) { + if (parent.type === "FunctionDeclaration") return null; + if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") { + const callback = parent; + let owner: ESTree.Node | null = callback.parent; + while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent; + if (owner?.type !== "CallExpression") return null; + const method = arrayMethodTarget(owner.callee); + const firstArgument = owner.arguments[0]; + if ( + method === null || (method.name !== "reduce" && method.name !== "reduceRight") || + owner.arguments.length > 2 || firstArgument === undefined || + unwrapArrayExpression(firstArgument) !== callback + ) return null; + const firstParameter = callback.params[0]; + const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter; + if (accumulator?.type !== "Identifier") return null; + return { callback, accumulator, initialValue: owner.arguments[1] }; + } + parent = parent.parent; + } + return null; +} + +function referencesAccumulator( + sourceCode: SourceCode, + node: ESTree.Node, + accumulator: Variable, + visited = new Set(), +): boolean { + const variable = resolveArrayBinding(sourceCode, node); + if (variable === null || visited.has(variable)) return false; + if (variable === accumulator) return true; + visited.add(variable); + if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false; + for (const definition of variable.defs) { + if ( + definition.type === "Variable" && definition.node.type === "VariableDeclarator" && + definition.node.id.type === "Identifier" && definition.node.init !== null && + definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const" + ) { + return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited); + } + } + return false; +} + +function isGlobalCopyOwner(sourceCode: SourceCode, node: ESTree.Node, name: string): boolean { + node = unwrapArrayExpression(node); + if (node.type !== "Identifier" || node.name !== name) return false; + const variable = resolveArrayBinding(sourceCode, node); + return variable === null || variable.defs.length === 0; +} + +/** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */ +export const noReduceAccumulatorCopyRule = defineRule({ + meta: { + type: "problem", + docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." }, + messages: { + accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.", + }, + }, + createOnce(context) { + return { + CallExpression(node) { + const method = arrayMethodTarget(node.callee); + if (method === null) return; + const reducer = enclosingReducer(node); + if (reducer === null) return; + const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable => + variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start), + ); + if (accumulator === undefined) return; + const isAccumulator = (expression: ESTree.Node) => + referencesAccumulator(context.sourceCode, expression, accumulator); + let copiesAccumulator = false; + if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) { + const target = node.arguments[0]; + copiesAccumulator = ( + target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" && + node.arguments.slice(1).some(isAccumulator) + ); + } else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) { + const source = node.arguments[0]; + copiesAccumulator = source !== undefined && isAccumulator(source); + } else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) { + const initialValue = reducer.initialValue; + const arrayAccumulator = initialValue !== undefined && + isKnownArrayExpression(context.sourceCode, initialValue); + copiesAccumulator = arrayAccumulator && isAccumulator(method.object); + } + if (copiesAccumulator) context.report({ node, messageId: "accumulatorCopy" }); + }, + }; + }, +}); diff --git a/skills/install-anti-slop/assets/anti-slop/rules/require-readable-spacing.ts b/skills/install-anti-slop/assets/anti-slop/rules/require-readable-spacing.ts new file mode 100644 index 0000000..fc5f9ca --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/rules/require-readable-spacing.ts @@ -0,0 +1,47 @@ +import type { CreateRule } from "@oxlint/plugins"; + +import createPaddingLineRule from "../vendor/eslint-stylistic/padding-line-between-statements.ts"; + +const paddingRule = createPaddingLineRule([ + { blankLine: "always", prev: "import", next: "*" }, + { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } }, + { blankLine: "always", prev: { selector: "Program > :not(ImportDeclaration)" }, next: "*" }, + { blankLine: "always", prev: "*", next: ["function", "class", "interface", "type"] }, + { blankLine: "always", prev: ["function", "class", "interface", "type"], next: "*" }, + { + blankLine: "always", + prev: "*", + next: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"], + }, + { + blankLine: "always", + prev: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"], + next: "*", + }, + { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] }, + { blankLine: "always", prev: "block-like", next: "*" }, + { blankLine: "any", prev: "import", next: "import" }, + { + blankLine: "any", + prev: { + selector: + ':matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type="TSDeclareFunction"])', + }, + next: { + selector: + ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])', + }, + }, +]); + +/** Restore structural blank lines with whitespace-only fixes; keep local short bindings and overloads grouped. */ +export const requireReadableSpacingRule: CreateRule = { + ...paddingRule, + meta: { + ...paddingRule.meta, + docs: { + description: "Require readable spacing between declarations and logical statement groups.", + }, + schema: [], + }, +}; diff --git a/skills/install-anti-slop/assets/anti-slop/shared/array-method.ts b/skills/install-anti-slop/assets/anti-slop/shared/array-method.ts new file mode 100644 index 0000000..87eb9d8 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/shared/array-method.ts @@ -0,0 +1,94 @@ +import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; + +/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */ +export function unwrapArrayExpression(node: ESTree.Node): ESTree.Node { + while ( + node.type === "ParenthesizedExpression" || + node.type === "ChainExpression" || + node.type === "TSAsExpression" || + node.type === "TSTypeAssertion" || + node.type === "TSNonNullExpression" || + node.type === "TSSatisfiesExpression" + ) { + node = node.expression; + } + return node; +} + +/** Resolve a local binding by scope, not by identifier spelling. */ +export function resolveArrayBinding(sourceCode: SourceCode, node: ESTree.Node): Variable | null { + node = unwrapArrayExpression(node); + if (node.type !== "Identifier") return null; + let scope: Scope | null = sourceCode.getScope(node); + while (scope !== null) { + const variable = scope.set.get(node.name); + if (variable !== undefined) return variable; + scope = scope.upper; + } + return null; +} + +/** Read static method names, including computed string literals, without evaluating expressions. */ +export function arrayMethodTarget( + node: ESTree.Node, +): { readonly name: string; readonly object: ESTree.Node } | null { + node = unwrapArrayExpression(node); + if (node.type !== "MemberExpression") return null; + const property = node.property; + if (!node.computed && property.type === "Identifier") { + return { name: property.name, object: node.object }; + } + if (node.computed && property.type === "Literal" && typeof property.value === "string") { + return { name: property.value, object: node.object }; + } + return null; +} + +function isArrayAnnotation(type: ESTree.TSType): boolean { + if (type.type === "TSArrayType" || type.type === "TSTupleType") return true; + if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation); + if (type.type === "TSTypeOperator" && type.operator === "readonly") { + return isArrayAnnotation(type.typeAnnotation); + } + return ( + type.type === "TSTypeReference" && type.typeName.type === "Identifier" && + (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray") + ); +} + +/** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */ +export function isKnownArrayExpression( + sourceCode: SourceCode, + node: ESTree.Node, + visited = new Set(), +): boolean { + node = unwrapArrayExpression(node); + if (node.type === "ArrayExpression") return true; + if (node.type === "CallExpression") { + const method = arrayMethodTarget(node.callee); + return ( + method !== null && + ["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) && + isKnownArrayExpression(sourceCode, method.object, visited) + ); + } + if (node.type !== "Identifier") return false; + const variable = resolveArrayBinding(sourceCode, node); + if (variable === null || visited.has(variable)) return false; + visited.add(variable); + if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false; + for (const identifier of variable.identifiers) { + const annotation = identifier.typeAnnotation?.typeAnnotation; + if (annotation !== undefined) return isArrayAnnotation(annotation); + } + for (const definition of variable.defs) { + if ( + definition.type === "Variable" && definition.node.type === "VariableDeclarator" && + definition.node.id.type === "Identifier" && definition.node.init !== null && + definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const" + ) { + return isKnownArrayExpression(sourceCode, definition.node.init, visited); + } + } + return false; +} diff --git a/skills/install-anti-slop/assets/anti-slop/shared/reflect-method.ts b/skills/install-anti-slop/assets/anti-slop/shared/reflect-method.ts index 39bc218..43a7cfc 100644 --- a/skills/install-anti-slop/assets/anti-slop/shared/reflect-method.ts +++ b/skills/install-anti-slop/assets/anti-slop/shared/reflect-method.ts @@ -1,17 +1,6 @@ -import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; +import { resolveVariable } from "./scope.ts"; -function resolveVariable( - sourceCode: SourceCode, - identifier: ESTree.IdentifierReference, -): Variable | null { - let scope: Scope | null = sourceCode.getScope(identifier); - while (scope !== null) { - const variable = scope.set.get(identifier.name); - if (variable !== undefined) return variable; - scope = scope.upper; - } - return null; -} +import type { ESTree, SourceCode } from "@oxlint/plugins"; function isGlobalReflect(sourceCode: SourceCode, expression: ESTree.Expression): boolean { if (expression.type !== "Identifier" || expression.name !== "Reflect") return false; diff --git a/skills/install-anti-slop/assets/anti-slop/shared/scope.ts b/skills/install-anti-slop/assets/anti-slop/shared/scope.ts new file mode 100644 index 0000000..602414a --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/shared/scope.ts @@ -0,0 +1,15 @@ +import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins"; + +/** Resolve an identifier to its binding by walking lexical scopes upward. */ +export function resolveVariable( + sourceCode: SourceCode, + identifier: ESTree.IdentifierReference, +): Variable | null { + let scope: Scope | null = sourceCode.getScope(identifier); + while (scope !== null) { + const variable = scope.set.get(identifier.name); + if (variable !== undefined) return variable; + scope = scope.upper; + } + return null; +} diff --git a/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/LICENSE b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/LICENSE new file mode 100644 index 0000000..38dbc35 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright OpenJS Foundation and other contributors, +Copyright (c) 2023-PRESENT ESLint Stylistic contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md new file mode 100644 index 0000000..3040694 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md @@ -0,0 +1,28 @@ +# Vendored padding-line-between-statements + +Source: [ESLint Stylistic](https://github.com/eslint-stylistic/eslint-stylistic), commit `435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08`. + +Copied files: + +- `packages/eslint-plugin/rules/padding-line-between-statements/padding-line-between-statements.ts` +- `packages/eslint-plugin/rules/padding-line-between-statements/types.d.ts` → `padding-line-options.d.ts` +- Root `LICENSE`, retained verbatim. Both OpenJS Foundation and ESLint Stylistic notices apply. + +The rule is MIT-licensed. Keep `LICENSE` with every redistributed copy, including skill assets. No Stylistic, ESLint, TypeScript-ESLint, or additional parser runtime dependency is required. + +## Local adaptations + +- Replace upstream type aliases with Oxlint's ESTree, context, token/comment, and rule types. Upstream's token type includes comments; Oxlint exposes those separately. +- Replace `AST_NODE_TYPES` enum members with identical string literals. +- Guard indexed reads for consuming repositories with `noUncheckedIndexedAccess`. Impossible missing AST/configuration entries raise explicit invariant errors rather than introducing new non-null assertions. +- Replace the repository-specific `createRule` factory with `createPaddingLineRule(options)`. The anti-slop wrapper supplies typed options directly; it exposes no user configuration options. +- Implement the small required AST helper surface in `padding-line-ast.ts` using Oxlint's public source-code/token API. `isParenthesized` only needs the one-pair check used to exclude parenthesized directive strings, not the upstream general-purpose overloads. +- Retain the upstream statement matchers, scope tracking, comment-aware insertion/removal, selector support, and diagnostic text. Upstream naming and non-null assumptions remain localized here to keep future diffs reviewable; the file is not a model for new application code. + +The opinionated policy lives outside this directory in `../../rules/require-readable-spacing.ts`. It adds spacing without collapsing existing blank lines. Short local bindings, consecutive imports, and adjacent overload signatures/implementation remain grouped. Spacing is syntactic, not an inference of business-logic boundaries. + +## Updating and verification + +Fetch an explicit upstream revision, compare the original rule and types against this revision, and port relevant fixes while retaining the adapters above. Update this record and preserve the license. Run `pnpm check` and `pnpm sync:skill-assets` as required by repository guidance. + +Focused Oxlint RuleTester cases live in `../../rules/require-readable-spacing.test.ts`; they test exact fixes, JSDoc/trailing comments, same-line statements, semicolon-free code, TypeScript exports/overloads, Effect-style generators, and upstream removal behavior. `../../rules/require-readable-spacing-cli.test.ts` verifies the exported plugin through the native Oxlint CLI on multiple files, including rejection, autofix, and repeated-fix stability. The complete upstream JS/TS test suites have not been ported; this is focused compatibility evidence, not a claim of full upstream conformance. diff --git a/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts new file mode 100644 index 0000000..10f34cc --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts @@ -0,0 +1,51 @@ +// Local replacements for the upstream helper imports. See UPSTREAM.md. +import type { ESTree, SourceCode, Token as SyntaxToken, Comment, Location } from "@oxlint/plugins"; + +type Token = SyntaxToken | Comment; + +/** Line terminators recognized by the upstream padding matcher. */ +export const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]); + +/** Test a closing brace without treating comment text as punctuation. */ +export const isClosingBraceToken = (token: Token): boolean => + token.type === "Punctuator" && token.value === "}"; + +/** Test a semicolon without treating comment text as punctuation. */ +export const isSemicolonToken = (token: Token): boolean => + token.type === "Punctuator" && token.value === ";"; + +/** Filter the optional final semicolon when identifying block-like statements. */ +export const isNotSemicolonToken = (token: Token): boolean => !isSemicolonToken(token); + +/** Compare token/node boundaries, including attached comments. */ +export const isTokenOnSameLine = (left: { loc: Location }, right: { loc: Location }): boolean => + left.loc.end.line === right.loc.start.line; + +/** Recognize declarations and expressions used by the upstream IIFE matcher. */ +export const isFunction = (node: ESTree.Node): boolean => + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression"; + +/** Preserve the upstream multiline statement heuristic. */ +export const isSingleLine = (node: ESTree.Node): boolean => + node.loc.start.line === node.loc.end.line; + +/** Unwrap optional chaining before checking IIFE syntax. */ +export const skipChainExpression = (node: ESTree.Node): ESTree.Node => + node.type === "ChainExpression" ? node.expression : node; + +/** Only a program or function-body expression can begin a directive prologue. */ +export const isTopLevelExpressionStatement = ( + node: ESTree.Node, +): node is ESTree.ExpressionStatement => + node.type === "ExpressionStatement" && + (node.parent.type === "Program" || + (node.parent.type === "BlockStatement" && isFunction(node.parent.parent))); + +/** A single wrapping pair suffices to exclude a string from directive syntax. */ +export function isParenthesized(node: ESTree.Node, sourceCode: SourceCode): boolean { + const before = sourceCode.getTokenBefore(node); + const after = sourceCode.getTokenAfter(node); + return before?.value === "(" && after?.value === ")"; +} diff --git a/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts new file mode 100644 index 0000000..c402db1 --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts @@ -0,0 +1,906 @@ +// Vendored from ESLint Stylistic; see UPSTREAM.md and LICENSE in this directory. +import type { ESTree, Context as RuleContext, SourceCode, Token as SyntaxToken, Comment, CreateRule, Location } from '@oxlint/plugins' +type ASTNode = ESTree.Node +type Token = SyntaxToken | Comment +import type { + RuleOptions, + SelectorOption, + StatementOption, +} from './padding-line-options.d.ts' +import { + isClosingBraceToken, + isFunction, + isNotSemicolonToken, + isParenthesized, + isSemicolonToken, + isSingleLine, + isTokenOnSameLine, + isTopLevelExpressionStatement, + LINEBREAKS, + skipChainExpression, +} from './padding-line-ast.ts' + +const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u +const CJS_IMPORT = /^require\(/u + +/** + * This rule is a replica of padding-line-between-statements. + * + * Ideally we would want to extend the rule support typescript specific support. + * But since not all the state is exposed by the eslint and eslint has frozen stylistic rules, + * (see - https://eslint.org/blog/2020/05/changes-to-rules-policies for details.) + * we are forced to re-implement the rule here. + * + * We have tried to keep the implementation as close as possible to the eslint implementation, to make + * patching easier for future contributors. + * + * Reference rule - https://github.com/eslint/eslint/blob/main/lib/rules/padding-line-between-statements.js + */ + +type NodeTest = ( + node: ASTNode, + sourceCode: SourceCode, +) => boolean + +interface NodeTestObject { + test: NodeTest +} + +const LT = `[${Array.from(LINEBREAKS).join('')}]` +const PADDING_LINE_SEQUENCE = new RegExp( + String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`, + 'u', +) + +function isSelectorOption(option: StatementOption): option is SelectorOption { + return typeof option === 'object' && !Array.isArray(option) +} + +/** + * Creates tester which check if a node starts with specific keyword with the + * appropriate AST_NODE_TYPES. + * @param keyword The keyword to test. + * @returns the created tester. + * @private + */ +function newKeywordTester( + type: string | string[], + keyword: string, +): NodeTestObject { + return { + test(node, sourceCode): boolean { + const isSameKeyword = sourceCode.getFirstToken(node)?.value === keyword + const isSameType = Array.isArray(type) + ? type.includes(node.type) + : type === node.type + + return isSameKeyword && isSameType + }, + } +} + +/** + * Creates tester which check if a node is specific type. + * @param type The node type to test. + * @returns the created tester. + * @private + */ +function newNodeTypeTester(type: string): NodeTestObject { + return { + test: (node): boolean => node.type === type, + } +} + +/** + * Checks the given node is an expression statement of IIFE. + * @param node The node to check. + * @returns `true` if the node is an expression statement of IIFE. + * @private + */ +function isIIFEStatement(node: ASTNode): boolean { + if (node.type === 'ExpressionStatement') { + let expression = skipChainExpression(node.expression) + if (expression.type === 'UnaryExpression') + expression = skipChainExpression(expression.argument) + + if (expression.type === 'CallExpression') { + let node: ASTNode = expression.callee + while (node.type === 'SequenceExpression') { + const lastExpression = node.expressions.at(-1) + if (lastExpression === undefined) + throw new Error('Padding rule invariant: sequence expression is empty') + node = lastExpression + } + + return isFunction(node) + } + } + return false +} + +/** + * Checks the given node is a CommonJS require statement + * @param node The node to check. + * @returns `true` if the node is a CommonJS require statement. + * @private + */ +function isCJSRequire(node: ASTNode): boolean { + if (node.type === 'VariableDeclaration') { + const declaration = node.declarations[0] + if (declaration?.init) { + let call = declaration?.init + while (call.type === 'MemberExpression') + call = call.object + + if ( + call.type === 'CallExpression' + && call.callee.type === 'Identifier' + ) { + return call.callee.name === 'require' + } + } + } + return false +} + +/** + * Checks whether the given node is a block-like statement. + * This checks the last token of the node is the closing brace of a block. + * @param sourceCode The source code to get tokens. + * @param node The node to check. + * @returns `true` if the node is a block-like statement. + * @private + */ +function isBlockLikeStatement( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + // do-while with a block is a block-like statement. + if ( + node.type === 'DoWhileStatement' + && node.body.type === 'BlockStatement' + ) { + return true + } + + /** + * IIFE is a block-like statement specially from + * JSCS#disallowPaddingNewLinesAfterBlocks. + */ + if (isIIFEStatement(node)) + return true + + // Checks the last token is a closing brace of blocks. + const lastToken = sourceCode.getLastToken(node, isNotSemicolonToken) + const belongingNode + = lastToken && isClosingBraceToken(lastToken) + ? sourceCode.getNodeByRangeIndex(lastToken.range[0]) + : null + + return ( + !!belongingNode + && (belongingNode.type === 'BlockStatement' + || belongingNode.type === 'SwitchStatement') + ) +} + +/** + * Check whether the given node is a directive or not. + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is a directive. + */ +function isDirective( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + return ( + isTopLevelExpressionStatement(node) + && node.expression.type === 'Literal' + && typeof node.expression.value === 'string' + && !isParenthesized(node.expression, sourceCode) + ) +} + +/** + * Check whether the given node is a part of directive prologue or not. + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is a part of directive prologue. + */ +function isDirectivePrologue( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + if ( + isDirective(node, sourceCode) + && node.parent + && 'body' in node.parent + && Array.isArray(node.parent.body) + ) { + for (const sibling of node.parent.body) { + if (sibling === node) + break + + if (!isDirective(sibling, sourceCode)) + return false + } + return true + } + return false +} + +/** + * Checks the given node is a CommonJS export statement + * @param node The node to check. + * @returns `true` if the node is a CommonJS export statement. + * @private + */ +function isCJSExport(node: ASTNode): boolean { + if (node.type === 'ExpressionStatement') { + const expression = node.expression + if (expression.type === 'AssignmentExpression') { + let left = expression.left + if (left.type === 'MemberExpression') { + while (left.object.type === 'MemberExpression') + left = left.object + + return ( + left.object.type === 'Identifier' + && (left.object.name === 'exports' + || (left.object.name === 'module' + && left.property.type === 'Identifier' + && left.property.name === 'exports')) + ) + } + } + } + return false +} + +/** + * Check whether the given node is an expression + * @param node The node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is an expression + */ +function isExpression( + node: ASTNode, + sourceCode: SourceCode, +): boolean { + return ( + node.type === 'ExpressionStatement' + && !isDirectivePrologue(node, sourceCode) + ) +} + +/** + * Gets the actual last token. + * + * If a semicolon is semicolon-less style's semicolon, this ignores it. + * For example: + * + * foo() + * ;[1, 2, 3].forEach(bar) + * @param sourceCode The source code to get tokens. + * @param node The node to get. + * @returns The actual last token. + * @private + */ +function getActualLastToken( + node: ASTNode, + sourceCode: SourceCode, +): Token | null { + const semiToken = sourceCode.getLastToken(node)! + const prevToken = sourceCode.getTokenBefore(semiToken) + const nextToken = sourceCode.getTokenAfter(semiToken) + const isSemicolonLessStyle + = prevToken + && nextToken + && prevToken.range[0] >= node.range[0] + && isSemicolonToken(semiToken) + && !isTokenOnSameLine(prevToken, semiToken) + && isTokenOnSameLine(semiToken, nextToken) + + return isSemicolonLessStyle ? prevToken : semiToken +} + +/** + * This returns the concatenation of the first 2 captured strings. + * @param _ Unused. Whole matched string. + * @param trailingSpaces The trailing spaces of the first line. + * @param indentSpaces The indentation spaces of the last line. + * @returns The concatenation of trailingSpaces and indentSpaces. + * @private + */ +function replacerToRemovePaddingLines( + _: string, + trailingSpaces: string, + indentSpaces: string, +): string { + return trailingSpaces + indentSpaces +} + +function getReportLoc(node: ASTNode, sourceCode: SourceCode): Location { + if (isSingleLine(node)) + return node.loc + + const line = node.loc.start.line + const sourceLine = sourceCode.lines[line - 1] + if (sourceLine === undefined) + throw new Error('Padding rule invariant: statement source line is missing') + + return { + start: node.loc.start, + end: { + line, + column: sourceLine.length, + }, + } +} + +/** + * Check and report statements for `any` configuration. + * It does nothing. + * + * @private + */ +function verifyForAny(): void { + // Empty +} + +/** + * Check and report statements for `never` configuration. + * This autofix removes blank lines between the given 2 statements. + * However, if comments exist between 2 blank lines, it does not remove those + * blank lines automatically. + * @param context The rule context to report. + * @param _ Unused. The previous node to check. + * @param nextNode The next node to check. + * @param paddingLines The array of token pairs that blank + * lines exist between the pair. + * + * @private + */ +function verifyForNever( + context: RuleContext, + _: ASTNode, + nextNode: ASTNode, + paddingLines: [Token, Token][], +): void { + if (paddingLines.length === 0) + return + + context.report({ + node: nextNode, + messageId: 'unexpectedBlankLine', + loc: getReportLoc(nextNode, context.sourceCode), + fix(fixer) { + if (paddingLines.length >= 2) + return null + + const paddingPair = paddingLines[0] + if (paddingPair === undefined) + throw new Error('Padding rule invariant: reported padding pair is missing') + const [prevToken, nextToken] = paddingPair + const start = prevToken.range[1] + const end = nextToken.range[0] + const text = context + .sourceCode + .text + .slice(start, end) + .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines) + + return fixer.replaceTextRange([start, end], text) + }, + }) +} + +/** + * Check and report statements for `always` configuration. + * This autofix inserts a blank line between the given 2 statements. + * If the `prevNode` has trailing comments, it inserts a blank line after the + * trailing comments. + * @param context The rule context to report. + * @param prevNode The previous node to check. + * @param nextNode The next node to check. + * @param paddingLines The array of token pairs that blank + * lines exist between the pair. + * + * @private + */ +function verifyForAlways( + context: RuleContext, + prevNode: ASTNode, + nextNode: ASTNode, + paddingLines: [Token, Token][], +): void { + if (paddingLines.length > 0) + return + + context.report({ + node: nextNode, + messageId: 'expectedBlankLine', + loc: getReportLoc(nextNode, context.sourceCode), + fix(fixer) { + const sourceCode = context.sourceCode + let prevToken = getActualLastToken(prevNode, sourceCode)! + const nextToken + = sourceCode.getFirstTokenBetween(prevToken, nextNode, { + includeComments: true, + + /** + * Skip the trailing comments of the previous node. + * This inserts a blank line after the last trailing comment. + * + * For example: + * + * foo(); // trailing comment. + * // comment. + * bar(); + * + * Get fixed to: + * + * foo(); // trailing comment. + * + * // comment. + * bar(); + * @param token The token to check. + * @returns `true` if the token is not a trailing comment. + * @private + */ + filter(token) { + if (isTokenOnSameLine(prevToken, token)) { + prevToken = token + return false + } + return true + }, + })! || nextNode + const insertText = isTokenOnSameLine(prevToken, nextToken) + ? '\n\n' + : '\n' + + return fixer.insertTextAfter(prevToken, insertText) + }, + }) +} + +/** + * Types of blank lines. + * `any`, `never`, and `always` are defined. + * Those have `verify` method to check and report statements. + * @private + */ +const PaddingTypes = { + any: { verify: verifyForAny }, + never: { verify: verifyForNever }, + always: { verify: verifyForAlways }, +} + +const MaybeMultilineStatementType: Record = { + 'block-like': { test: isBlockLikeStatement }, + 'expression': { test: isExpression }, + 'return': newKeywordTester('ReturnStatement', 'return'), + 'export': newKeywordTester( + [ + 'ExportAllDeclaration', + 'ExportDefaultDeclaration', + 'ExportNamedDeclaration', + ], + 'export', + ), + 'var': newKeywordTester('VariableDeclaration', 'var'), + 'let': newKeywordTester('VariableDeclaration', 'let'), + 'const': newKeywordTester('VariableDeclaration', 'const'), + 'using': { + test: node => node.type === 'VariableDeclaration' + && (node.kind === 'using' || node.kind === 'await using'), + }, + 'type': newKeywordTester('TSTypeAliasDeclaration', 'type'), +} + +/** + * Types of statements. + * Those have `test` method to check it matches to the given statement. + * @private + */ +const StatementTypes: Record = { + '*': { test: (): boolean => true }, + 'exports': { test: isCJSExport }, + 'require': { test: isCJSRequire }, + 'directive': { test: isDirectivePrologue }, + 'iife': { test: isIIFEStatement }, + + 'block': newNodeTypeTester('BlockStatement'), + 'empty': newNodeTypeTester('EmptyStatement'), + 'function': newNodeTypeTester('FunctionDeclaration'), + 'ts-method': newNodeTypeTester('TSMethodSignature'), + + 'break': newKeywordTester('BreakStatement', 'break'), + 'case': newKeywordTester('SwitchCase', 'case'), + 'class': newKeywordTester('ClassDeclaration', 'class'), + 'continue': newKeywordTester('ContinueStatement', 'continue'), + 'debugger': newKeywordTester('DebuggerStatement', 'debugger'), + 'default': newKeywordTester( + ['SwitchCase', 'ExportDefaultDeclaration'], + 'default', + ), + 'do': newKeywordTester('DoWhileStatement', 'do'), + 'for': newKeywordTester( + [ + 'ForStatement', + 'ForInStatement', + 'ForOfStatement', + ], + 'for', + ), + 'if': newKeywordTester('IfStatement', 'if'), + 'import': newKeywordTester('ImportDeclaration', 'import'), + 'switch': newKeywordTester('SwitchStatement', 'switch'), + 'throw': newKeywordTester('ThrowStatement', 'throw'), + 'try': newKeywordTester('TryStatement', 'try'), + 'while': newKeywordTester( + ['WhileStatement', 'DoWhileStatement'], + 'while', + ), + 'with': newKeywordTester('WithStatement', 'with'), + + 'cjs-export': { + test: (node, sourceCode) => node.type === 'ExpressionStatement' + && node.expression.type === 'AssignmentExpression' + && CJS_EXPORT.test(sourceCode.getText(node.expression.left)), + }, + 'cjs-import': { + test: (node, sourceCode) => node.type === 'VariableDeclaration' + && node.declarations.length > 0 + && node.declarations[0]?.init != null + && CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)), + }, + + 'enum': newKeywordTester( + 'TSEnumDeclaration', + 'enum', + ), + 'interface': newKeywordTester( + 'TSInterfaceDeclaration', + 'interface', + ), + 'function-overload': newNodeTypeTester('TSDeclareFunction'), + ...Object.fromEntries( + Object.entries(MaybeMultilineStatementType) + .flatMap(([key, value]) => [ + [key, value], + [ + `singleline-${key}`, + { + ...value, + test: (node, sourceCode) => value.test(node, sourceCode) && isSingleLine(node), + }, + ], + [ + `multiline-${key}`, + { + ...value, + test: (node, sourceCode) => value.test(node, sourceCode) && !isSingleLine(node), + }, + ], + ]), + ), +} + +/** Build the vendored padding rule with caller-owned, typed policy options. */ +export default function createPaddingLineRule(options: RuleOptions): CreateRule { +return { + meta: { + type: 'layout', + docs: { + description: 'Require or disallow padding lines between statements', + }, + fixable: 'whitespace', + hasSuggestions: false, + // This is intentionally an array schema as you can pass 0..n config objects + schema: { + $defs: { + paddingType: { + type: 'string', + enum: Object.keys(PaddingTypes), + }, + statementType: { + type: 'string', + enum: Object.keys(StatementTypes), + }, + selectorOption: { + type: 'object', + properties: { + selector: { + type: 'string', + }, + lineMode: { + type: 'string', + enum: ['any', 'singleline', 'multiline'], + }, + }, + required: ['selector'], + additionalProperties: false, + }, + statementMatcher: { + anyOf: [ + { $ref: '#/$defs/statementType' }, + { $ref: '#/$defs/selectorOption' }, + ], + }, + statementOption: { + anyOf: [ + { $ref: '#/$defs/statementMatcher' }, + { + type: 'array', + items: { $ref: '#/$defs/statementMatcher' }, + minItems: 1, + uniqueItems: true, + additionalItems: false, + }, + ], + }, + }, + type: 'array', + additionalItems: false, + items: { + type: 'object', + properties: { + blankLine: { $ref: '#/$defs/paddingType' }, + prev: { $ref: '#/$defs/statementOption' }, + next: { $ref: '#/$defs/statementOption' }, + }, + additionalProperties: false, + required: ['blankLine', 'prev', 'next'], + }, + }, + messages: { + unexpectedBlankLine: 'Unexpected blank line before this statement.', + expectedBlankLine: 'Expected blank line before this statement.', + }, + }, + create(context) { + const sourceCode = context.sourceCode + + const selectorMatchedNodes = new Map>() + const pendingPairs: { prevNode: ASTNode, nextNode: ASTNode }[] = [] + + function collectSelectorOption(option: StatementOption): void { + if (Array.isArray(option)) { + for (const item of option) + collectSelectorOption(item) + return + } + + if (!isSelectorOption(option)) + return + + selectorMatchedNodes.set(option.selector, new Set()) + } + + for (const configure of options) { + collectSelectorOption(configure.prev) + collectSelectorOption(configure.next) + } + + type Scope = { + upper: Scope + prevNode: ASTNode | null + } | null + + let scopeInfo: Scope = null + + /** + * Processes to enter to new scope. + * This manages the current previous statement. + * + * @private + */ + function enterScope(): void { + scopeInfo = { + upper: scopeInfo, + prevNode: null, + } + } + + /** + * Processes to exit from the current scope. + * + * @private + */ + function exitScope(): void { + if (scopeInfo) + scopeInfo = scopeInfo.upper + } + + /** + * Checks whether the given node matches the given type. + * @param node The statement node to check. + * @param type The statement type to check. + * @returns `true` if the statement node matched the type. + * @private + */ + function match(node: ASTNode, type: StatementOption): boolean { + let innerStatementNode = node + + while (innerStatementNode.type === 'LabeledStatement') + innerStatementNode = innerStatementNode.body + + if (Array.isArray(type)) + return type.some(match.bind(null, innerStatementNode)) + + if (isSelectorOption(type)) { + const matchedNodes = selectorMatchedNodes.get(type.selector) + if (!matchedNodes?.has(innerStatementNode)) + return false + + const lineMode = type.lineMode + + if (lineMode === 'singleline') + return isSingleLine(innerStatementNode) + else if (lineMode === 'multiline') + return !isSingleLine(innerStatementNode) + + return true + } + else { + const statementType = StatementTypes[type] + if (statementType === undefined) + throw new Error(`Padding rule invariant: unsupported statement type ${type}`) + return statementType.test(innerStatementNode, sourceCode) + } + } + + /** + * Finds the last matched configure from options. + * @param prevNode The previous statement to match. + * @param nextNode The current statement to match. + * @returns The tester of the last matched configure. + * @private + */ + function getPaddingType( + prevNode: ASTNode, + nextNode: ASTNode, + ): (typeof PaddingTypes)[keyof typeof PaddingTypes] { + for (let i = options.length - 1; i >= 0; --i) { + const configure = options[i] + if (configure === undefined) + throw new Error('Padding rule invariant: configuration entry is missing') + if ( + match(prevNode, configure.prev) + && match(nextNode, configure.next) + ) { + return PaddingTypes[configure.blankLine] + } + } + return PaddingTypes.any + } + + /** + * Gets padding line sequences between the given 2 statements. + * Comments are separators of the padding line sequences. + * @param prevNode The previous statement to count. + * @param nextNode The current statement to count. + * @returns The array of token pairs. + * @private + */ + function getPaddingLineSequences( + prevNode: ASTNode, + nextNode: ASTNode, + ): [Token, Token][] { + const pairs: [Token, Token][] = [] + let prevToken: Token = getActualLastToken(prevNode, sourceCode)! + + if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) { + do { + const token: Token = sourceCode.getTokenAfter(prevToken, { + includeComments: true, + })! + + if (token.loc.start.line - prevToken.loc.end.line >= 2) + pairs.push([prevToken, token]) + + prevToken = token + } while (prevToken.range[0] < nextNode.range[0]) + } + + return pairs + } + + /** + * Verify padding lines between the given node and the previous node. + * @param node The node to verify. + * + * @private + */ + function verify(node: ASTNode): void { + if ( + !node.parent + || ![ + 'BlockStatement', + 'Program', + 'StaticBlock', + 'SwitchCase', + 'SwitchStatement', + 'TSInterfaceBody', + 'TSModuleBlock', + 'TSTypeLiteral', + ].includes(node.parent.type) + ) { + return + } + + // Save this node as the current previous statement. + const prevNode = scopeInfo!.prevNode + + // Verify. + if (prevNode) + pendingPairs.push({ prevNode, nextNode: node }) + + scopeInfo!.prevNode = node + } + + function verifyPendingPairs(): void { + for (const { prevNode, nextNode } of pendingPairs) { + const type = getPaddingType(prevNode, nextNode) + const paddingLines = getPaddingLineSequences(prevNode, nextNode) + + type.verify(context, prevNode, nextNode, paddingLines) + } + } + + /** + * Verify padding lines between the given node and the previous node. + * Then process to enter to new scope. + * @param node The node to verify. + * + * @private + */ + function verifyThenEnterScope(node: ASTNode): void { + verify(node) + enterScope() + } + + const selectorMatchListeners = Object.fromEntries( + Array.from(selectorMatchedNodes.keys(), selector => [ + selector, + (node: ASTNode): void => { + selectorMatchedNodes.get(selector)?.add(node) + }, + ]), + ) + + return { + 'Program': enterScope, + 'Program:exit': () => { + verifyPendingPairs() + exitScope() + }, + 'BlockStatement': enterScope, + 'BlockStatement:exit': exitScope, + 'SwitchStatement': enterScope, + 'SwitchStatement:exit': exitScope, + 'SwitchCase': verifyThenEnterScope, + 'SwitchCase:exit': exitScope, + 'StaticBlock': enterScope, + 'StaticBlock:exit': exitScope, + + 'TSInterfaceBody': enterScope, + 'TSInterfaceBody:exit': exitScope, + 'TSModuleBlock': enterScope, + 'TSModuleBlock:exit': exitScope, + 'TSTypeLiteral': enterScope, + 'TSTypeLiteral:exit': exitScope, + 'TSDeclareFunction': verifyThenEnterScope, + 'TSDeclareFunction:exit': exitScope, + 'TSMethodSignature': verifyThenEnterScope, + 'TSMethodSignature:exit': exitScope, + + ':statement': verify, + ...selectorMatchListeners, + } + }, +} +} diff --git a/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts new file mode 100644 index 0000000..45068ec --- /dev/null +++ b/skills/install-anti-slop/assets/anti-slop/vendor/eslint-stylistic/padding-line-options.d.ts @@ -0,0 +1,87 @@ +/* GENERATED, DO NOT EDIT DIRECTLY */ + +/* @checksum: 3QCTtOH6rJM5_AGJ58rGpeEaBEfaJz17MSCxWB4X_PU */ + +export type PaddingType = 'any' | 'never' | 'always' +export type StatementOption = + | StatementMatcher + | [StatementMatcher, ...StatementMatcher[]] +export type StatementMatcher = + | StatementType + | SelectorOption +export type StatementType = + | '*' + | 'exports' + | 'require' + | 'directive' + | 'iife' + | 'block' + | 'empty' + | 'function' + | 'ts-method' + | 'break' + | 'case' + | 'class' + | 'continue' + | 'debugger' + | 'default' + | 'do' + | 'for' + | 'if' + | 'import' + | 'switch' + | 'throw' + | 'try' + | 'while' + | 'with' + | 'cjs-export' + | 'cjs-import' + | 'enum' + | 'interface' + | 'function-overload' + | 'block-like' + | 'singleline-block-like' + | 'multiline-block-like' + | 'expression' + | 'singleline-expression' + | 'multiline-expression' + | 'return' + | 'singleline-return' + | 'multiline-return' + | 'export' + | 'singleline-export' + | 'multiline-export' + | 'var' + | 'singleline-var' + | 'multiline-var' + | 'let' + | 'singleline-let' + | 'multiline-let' + | 'const' + | 'singleline-const' + | 'multiline-const' + | 'using' + | 'singleline-using' + | 'multiline-using' + | 'type' + | 'singleline-type' + | 'multiline-type' +export type PaddingLineBetweenStatementsSchema0 = { + blankLine: PaddingType + prev: StatementOption + next: StatementOption +}[] + +export interface SelectorOption { + selector: string + lineMode?: 'any' | 'singleline' | 'multiline' +} + +export type PaddingLineBetweenStatementsRuleOptions + = PaddingLineBetweenStatementsSchema0 + +export type RuleOptions + = PaddingLineBetweenStatementsRuleOptions +export type MessageIds = + | 'unexpectedBlankLine' + | 'expectedBlankLine' diff --git a/tests/fixtures/anti-slop-c44ef22-manifest.json b/tests/fixtures/anti-slop-c44ef22-manifest.json new file mode 100644 index 0000000..dd6695b --- /dev/null +++ b/tests/fixtures/anti-slop-c44ef22-manifest.json @@ -0,0 +1,40 @@ +{ + "effect/index.ts": "55eb13ddeace8e2f4a7005511c2a68a17d4fc2cf", + "effect/rules/no-manual-effect-error-tag.ts": "daaa109ae95a6448b05194d8afb203831d51158d", + "effect/rules/no-manual-tag-comparison.ts": "868d1d274bf9eb8697d93db581201f9bca8d7a6e", + "effect/rules/no-manual-tagged-construction.ts": "382f166a1f0bdfcced51daf85653199702c467a9", + "effect/rules/no-service-constructor-imports.ts": "55cefb7e7b4c8e0330ccf66ab71096a3976e1f18", + "effect/rules/prefer-effect-match.ts": "d834540343ef1933adbe61e9eecbb6c8e9465372", + "effect/shared/tagged-values.ts": "21d10a2f8d9f97af79bbbc2d15155c4322dbaeb0", + "index.ts": "a3876ec07e4548f66ee353096b3ac851b7572f5b", + "rules/no-array-filter-map.ts": "dc001b381465420ec98111507f1cde2e6c92fb6f", + "rules/no-chained-type-assertions.ts": "0d118527804cfd00fe377af13293f3d2071492bf", + "rules/no-conditional-empty-object-spread.ts": "ae7248d36e54851b67ad1f5d78813bf09ac3dd6a", + "rules/no-known-value-widening.ts": "0df39ab89cc2d9fe482c92b55ef04ae2c245b46b", + "rules/no-module-mocking.ts": "2d60d48c998cbedfb84f3a09daec6ca52e3e6de5", + "rules/no-object-parameters.ts": "6589ebf5f587ecdad1989b9f89886370f0ed5350", + "rules/no-reduce-accumulator-copy.ts": "47d8e2cf6b5c640b0a587b65457a543747123bac", + "rules/no-reflect-apply.ts": "2cc30451bda68d90d84eb9744034a778ae783b20", + "rules/no-reflect-get.ts": "cf630ecc005aa9cef09cdd4a701feea968674b47", + "rules/no-runtime-typeof.ts": "43259eb9e65b480b3b64b03a4de0154511efcb51", + "rules/no-shape-in-symbol-names.ts": "436d2a25255b4aa43b50cc117e8f15539924dbd8", + "rules/no-unknown-parameters.ts": "b4a1545a7b2bc54b5eb8d5ce3090bf151d0cd6e0", + "rules/no-unknown-returns.ts": "e1f43f8d055f848c61fda84e907d0dea4079c3ba", + "rules/no-unknown-type-aliases.ts": "af5f08a5002fb26f94414be13f1ad848ae73d522", + "rules/no-unsafe-dictionary-type.ts": "8cb615a14f97769bd8e37465a8334edd338dc839", + "rules/no-widen-then-assert.ts": "c5e07f7fc55a8ebc1c19db58625552822beb0c1e", + "rules/require-readable-spacing.ts": "fc5f9ca5a6e6f9fee99358d2e39b81aa5fdb20fc", + "rules/require-safety-comment-for-type-assertion.ts": "bfea1a23a8358caf125b083452425649c7abe29d", + "shared/array-method.ts": "87eb9d842f4d8b4cc0b8e26a7e686034abd4d828", + "shared/dictionary-types.ts": "8db73ffab0fceb26cf6c0c534b3c4da71436396f", + "shared/function-parameters.ts": "80de91d7a75258044257d6e89ba666c29c284609", + "shared/lexical-type-parameters.ts": "7cdb18c911c9dd31066ead3beda0a9dfc6554018", + "shared/reflect-method.ts": "43a7cfcc12e9e0bff173d1246d38ba880d83ed07", + "shared/scope.ts": "602414abaa2ac38d778f52b452d3a7f7fad81fa3", + "shared/type-alias-resolution.ts": "4744222a0a30874037eb8354b1aa744072c50330", + "vendor/eslint-stylistic/LICENSE": "38dbc3547844ce7d5479008d9db1c042e1505b36", + "vendor/eslint-stylistic/UPSTREAM.md": "3040694e10e100718e2c43d6b15201a2f1a59016", + "vendor/eslint-stylistic/padding-line-ast.ts": "10f34cc8e5fcdd71ec6eed40e4369c6e4bfc7b89", + "vendor/eslint-stylistic/padding-line-between-statements.ts": "c402db1b26bf026fee244f9c7e7f3e24d74a2cb4", + "vendor/eslint-stylistic/padding-line-options.d.ts": "45068ec5246b423cd44ec6329185e0a30267ba72" +} From eee89204b9e583a39ecc15526ac6972ab8cd54c2 Mon Sep 17 00:00:00 2001 From: Fahmi Harun <34875577+kuker24@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:58:46 +0700 Subject: [PATCH 2/4] feat(anti-slop): reconcile profiles and non-destructive update --- skills/install-anti-slop/SKILL.md | 25 ++-- .../install-anti-slop/references/profiles.md | 17 ++- skills/install-anti-slop/references/rules.md | 22 ++- skills/install-anti-slop/references/update.md | 101 +++++++++++++ skills/install-anti-slop/scripts/install.mjs | 10 +- skills/install-anti-slop/scripts/manage.mjs | 136 +++++++++++++++++- 6 files changed, 291 insertions(+), 20 deletions(-) create mode 100644 skills/install-anti-slop/references/update.md diff --git a/skills/install-anti-slop/SKILL.md b/skills/install-anti-slop/SKILL.md index ccc647e..19059a4 100644 --- a/skills/install-anti-slop/SKILL.md +++ b/skills/install-anti-slop/SKILL.md @@ -8,14 +8,14 @@ license: MIT # install-anti-slop OpenCodeHighEnd adapter for installing, auditing, configuring, or removing Anti-Slop Oxlint rules. -Vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT, Dillon Mulroy, commit `e8c4880471b23ab7f216fba7b27d173a6ef07d4c`, v0.1.2). +Vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT, Dillon Mulroy, commit `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`, version `0.1.2+c44ef22 (untagged main, post-v0.1.2)`). ## Core Boundaries 1. **Opt-In Only**: Load this skill ONLY when the user explicitly requests Anti-Slop (e.g. "pasang anti-slop", "audit anti-slop", "hapus anti-slop"). Never auto-load during ordinary coding or non-TS/JS tasks. -2. **Distinct from `/unslop` and UI Craft**: `/unslop` and `rules/03-prose-discipline.md` handle prose cleanup. UI template anti-patterns (e.g. default purple gradient mesh, Inter-on-white-card slop, fake testimonials) live in `skills/impeccable/reference/taste-guard.md`. `install-anti-slop` is strictly for static Oxlint linting of TypeScript/JavaScript code. +2. **Distinct from `/unslop` and UI Craft**: `/unslop` and `rules/03-prose-discipline.md` handle prose cleanup. UI template anti-patterns live in `skills/impeccable/reference/taste-guard.md`. `install-anti-slop` is strictly for static Oxlint linting of TypeScript/JavaScript code. 3. **No OCBF Core Coupling**: Never add Oxlint or Anti-Slop to OCBF's core Python codebase or dependencies. -4. **Exact Version Coupling**: Keep `oxlint` and `@oxlint/plugins` on the exact same version. +4. **Exact Version Coupling**: Keep `oxlint` and `@oxlint/plugins` on the exact same version (1.78.0). 5. **No Blind Global Rewrites**: Linter findings identify patterns; resolve root causes with inference, `satisfies`, and boundary validation rather than casts or fake comments. ## The 5 Modes @@ -23,12 +23,13 @@ Vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT, | Mode | Behavior | |---|---| | `audit` | Evaluates rules against source/test/tooling; reports findings without modifying files or dependencies. | -| `recommended` | Installs curated OCBF profile (high-signal type safety assertions) after baseline review. | -| `strict` | Enables all 15 generic upstream rules (requires explicit user confirmation). | +| `recommended` | Installs curated OCBF profile (4 high-signal assertions) after baseline review. | +| `strict` | Enables all 18 generic upstream rules + native companion `oxc/no-accumulating-spread` (requires explicit user confirmation). | | `custom` | Enables user-selected rule set. | -| `update` | Refreshes vendored rules and assets while preserving current profile preferences. | +| `update` | Non-destructive dry-run comparison (ADD/CHANGE/SAME/LOCAL-ONLY); applies overwrite and reconciliation only with `--force` (see [references/update.md](references/update.md)). | -*Effect Rule Group*: Opt-in separately (`--with-effect`) only if `effect` is a direct project dependency. +*Effect Rule Group*: Opt-in separately (`--with-effect`) only if `effect` is a direct project dependency (5 rules). +*Vendored Dependencies*: Includes upstream-adapted `eslint-stylistic` (MIT) under `assets/anti-slop/vendor/eslint-stylistic/`; retain its `LICENSE` and `UPSTREAM.md`. ## Usage @@ -47,10 +48,13 @@ node /scripts/manage.mjs install --profile strict # 4. Install with Effect rules (when project uses Effect) node /scripts/manage.mjs install --profile recommended --with-effect -# 5. Update existing installation -node /scripts/manage.mjs update --profile recommended +# 5. Non-destructive update review (dry run) +node /scripts/manage.mjs update -# 6. Safe removal +# 6. Apply update with force +node /scripts/manage.mjs update --force --profile recommended + +# 7. Safe removal node /scripts/manage.mjs remove ``` @@ -58,3 +62,4 @@ node /scripts/manage.mjs remove - Detailed profile definitions: [references/profiles.md](references/profiles.md) - Complete rule documentation & fixes: [references/rules.md](references/rules.md) +- Update workflow & merge doctrine: [references/update.md](references/update.md) diff --git a/skills/install-anti-slop/references/profiles.md b/skills/install-anti-slop/references/profiles.md index 1328946..fc85eae 100644 --- a/skills/install-anti-slop/references/profiles.md +++ b/skills/install-anti-slop/references/profiles.md @@ -15,23 +15,28 @@ The recommended profile enables high-signal rules that catch artificial type evi ### Excluded from Recommended Default These rules are reserved for the `strict` or `custom` profiles: +- `no-array-filter-map`: Stylistic/performance preference for iterator helper pipelines; can be overly opinionated on legacy array pipelines. - `no-module-mocking`: Too disruptive for existing test suites that legitimately mock external SDKs/transports. +- `no-reduce-accumulator-copy`: Paired with native `oxc/no-accumulating-spread`; reserved for strict audit of O(n^2) accumulator spreads. - `no-runtime-typeof`: `typeof` is valid in projects without schema decoding libraries. - `no-shape-in-symbol-names`: Pure naming convention, not a type-correctness proof. - `no-conditional-empty-object-spread`: Depends on object exact-optional semantics. - `no-object-parameters`, `no-unknown-*`, `no-unsafe-dictionary-type`: Too broad as universal standards. - `no-reflect-get`, `no-reflect-apply`: Useful only where project policy specifically forbids Reflect metaprogramming. +- `require-readable-spacing`: Whitespace padding rule adapted from eslint-stylistic; reserved for strict profile to avoid conflicting with existing formatters (Prettier, Biome). --- ## 2. Strict Profile -Enables all 15 generic upstream rules as `"error"`: +Enables all 18 generic upstream rules as `"error"` plus native companion `"oxc/no-accumulating-spread"`: +- `anti-slop/no-array-filter-map`: `"error"` - `anti-slop/no-chained-type-assertions`: `"error"` - `anti-slop/no-conditional-empty-object-spread`: `"error"` - `anti-slop/no-known-value-widening`: `"error"` - `anti-slop/no-module-mocking`: `"error"` - `anti-slop/no-object-parameters`: `"error"` +- `anti-slop/no-reduce-accumulator-copy`: `"error"` - `anti-slop/no-reflect-apply`: `"error"` - `anti-slop/no-reflect-get`: `"error"` - `anti-slop/no-runtime-typeof`: `"error"` @@ -41,12 +46,22 @@ Enables all 15 generic upstream rules as `"error"`: - `anti-slop/no-unknown-type-aliases`: `"error"` - `anti-slop/no-unsafe-dictionary-type`: `"error"` - `anti-slop/no-widen-then-assert`: `"error"` +- `anti-slop/require-readable-spacing`: `"error"` - `anti-slop/require-safety-comment-for-type-assertion`: `"error"` +- `oxc/no-accumulating-spread`: `"error"` --- ## 3. Effect Rule Group (Opt-In) Enables Effect-specific architectural discipline: +- `anti-slop-effect/no-manual-effect-error-tag`: `"error"` + Rejects manual `_tag` property assignment on error classes; use `Data.TaggedError` instead. +- `anti-slop-effect/no-manual-tag-comparison`: `"error"` + Rejects direct `_tag === "..."` comparisons; use `Match.tag` or `Predicate.isTagged` instead. +- `anti-slop-effect/no-manual-tagged-construction`: `"error"` + Rejects manual construction of tagged objects; use constructors like `Either.right(...)` instead. - `anti-slop-effect/no-service-constructor-imports`: `"error"` Rejects named `make` imports from project modules outside tests. Callers must import the owning Layer and yield the contextual service. +- `anti-slop-effect/prefer-effect-match`: `"error"` + Prefers `Match.type().pipe(...)` over nested ternary or switch statements on `_tag`. diff --git a/skills/install-anti-slop/references/rules.md b/skills/install-anti-slop/references/rules.md index c1655fa..4fad25f 100644 --- a/skills/install-anti-slop/references/rules.md +++ b/skills/install-anti-slop/references/rules.md @@ -15,11 +15,31 @@ Guidelines for addressing Anti-Slop linter findings correctly without compromisi - DO NOT delete valid regression tests to silence `no-module-mocking`. - DO NOT weaken public API signatures without assessing breaking impact. -## Rule Summaries +## Generic Rule Summaries +- **`no-array-filter-map`**: `arr.filter(predicate).map(fn)` -> Use a single-pass combination (e.g. `flatMap` or loop), or iterator helpers `.values().filter().map().toArray()` only when the runtime environment supports ECMAScript iterator helpers. - **`no-chained-type-assertions`**: `x as object as User` -> Validate input at boundary with schema. - **`no-conditional-empty-object-spread`**: `...(cond ? { a } : {})` -> Assign conditionally or assemble object imperatively. - **`no-known-value-widening`**: `const m: Record = { k: v }` -> Use `satisfies Record`. - **`no-module-mocking`**: `vi.mock("./store")` -> Inject test doubles through constructor/function parameters. +- **`no-object-parameters`**: `function f(opts: object)` -> Define explicit interface or type alias for parameters. +- **`no-reduce-accumulator-copy`**: `arr.reduce((acc, x) => ({ ...acc, [x.id]: x }), {})` -> Mutate accumulator in-place `acc[x.id] = x; return acc;` or use `Object.fromEntries` to avoid O(n^2) shallow cloning. +- **`no-reflect-apply`**: `Reflect.apply(fn, ctx, args)` -> Call function directly or via `fn.apply(ctx, args)`. +- **`no-reflect-get`**: `Reflect.get(obj, key)` -> Access properties directly with bracket or dot notation. +- **`no-runtime-typeof`**: `typeof x === "string"` -> Use schema parsing or tagged unions rather than raw typeof. +- **`no-shape-in-symbol-names`**: Naming symbols with shape words (e.g. `IUser`, `UserType`) -> Use canonical domain names. +- **`no-unknown-parameters`**: `function f(x: unknown)` -> Constrain parameter types with generics or schemas. +- **`no-unknown-returns`**: `function f(): unknown` -> Return concrete types or discriminated unions. +- **`no-unknown-type-aliases`**: `type T = unknown` -> Define concrete or branded types. +- **`no-unsafe-dictionary-type`**: `Record` -> Use typed key/value pairs or schema boundaries. - **`no-widen-then-assert`**: `const x: unknown = val; (x as Target)` -> Preserve variable's original static type. +- **`require-readable-spacing`**: Enforces readable newline padding between logical statement blocks (adapted from `eslint-stylistic/padding-line-between-statements`). - **`require-safety-comment-for-type-assertion`**: Add `// SAFETY: `. + +## Effect Rule Summaries + +- **`no-manual-effect-error-tag`**: `class MyError { readonly _tag = "MyError"; }` -> Use `Data.TaggedError("MyError")`. +- **`no-manual-tag-comparison`**: `if (val._tag === "SomeTag")` -> Use `Match.tag` or `Predicate.isTagged` from Effect. +- **`no-manual-tagged-construction`**: `{ _tag: "Right", right: value }` -> Use constructor functions like `Either.right(value)`. +- **`no-service-constructor-imports`**: Named `make` imports -> Import the owning Layer and yield the contextual service. +- **`prefer-effect-match`**: Nested ternary or switch on `_tag` -> Use `Match.type().pipe(Match.tag(...), Match.exhaustive)`. diff --git a/skills/install-anti-slop/references/update.md b/skills/install-anti-slop/references/update.md new file mode 100644 index 0000000..b2e7f67 --- /dev/null +++ b/skills/install-anti-slop/references/update.md @@ -0,0 +1,101 @@ +# Update a vendored installation + +Use this procedure for an existing anti-slop installation. Local edits are owned policy, not drift to erase. An update is a reviewed merge, not a directory replacement. + +## 1. Establish scope and protect local work + +Locate the active generic and optional Effect entry points through lint configuration, then inspect their rules, helpers, tests, options, severities, overrides, and dependency versions. Read any `UPSTREAM.md` or equivalent provenance record and relevant Git history. Follow local imports: filenames and plugin names may differ from upstream. + +Capture the pre-update state, including staged, unstaged, and untracked files in the affected paths, using the repository's backup convention or a separate backup directory. Keep this backup outside the merge destination. Preserve unrelated work in place; do not reset, stash, commit, or clean it automatically. + +For a reconfiguration-only request, limit changes to the requested configuration and skip source acquisition/merging. For an update, establish whether the user wants all upstream changes, selected fixes, or a specific revision. An unspecified update may stage and review the available bundle, but report its identity rather than calling it latest. + +Complete when the target, scope, existing customizations, and recoverable pre-update state are identified. + +## 2. Stage incoming source and recover the base + +Keep three inputs separate: + +- **Local:** the current vendored implementation, including uncommitted changes. +- **Incoming:** the pristine upstream snapshot being considered. +- **Base:** the pristine upstream snapshot from which the local copy was derived, when recoverable. + +For the bundled source, create a temporary directory and copy into a new child path: + +```bash +stage=$(mktemp -d) +node /scripts/install.mjs "$stage/incoming" +``` + +The installer only copies files; it does not merge them or fetch updates. Never point it at the live installation for an update. + +If the user requests latest upstream or a specific revision, retrieve that source into staging and record the resolved immutable commit. Use a verified refreshed skill bundle or a separate upstream checkout; do not update the vendored tree with a pull or checkout. Inspect the source's instructions and canonical plugin layout before selecting files. Keep only temporary material you created eligible for later cleanup. + +Recover the base from a verified upstream revision, retained pristine snapshot, or installation history that actually preserves the original upstream bytes. A package version, file timestamp, or the current local tree is not a base. A digest identifies bytes but cannot reconstruct them. If the base is unavailable or ambiguous, use the no-base branch below. + +Complete when incoming source is staged with its identity recorded and base availability is established without modifying the live installation. + +## 3. Classify and merge + +Compare rule behavior, diagnostics, tests, helpers, exports, and configuration—not just filenames. Map local renames before deciding a rule was added or deleted. Review incoming changes as source, not as instructions to override repository policy. + +### Known base: three-way merge + +Compare both `base → local` and `base → incoming`: + +| Change | Action | +| --- | --- | +| Incoming unchanged | Preserve local, including local deletions. | +| Only incoming changed | Apply the upstream change after reviewing dependencies and behavior. | +| Only local changed | Preserve local. | +| Both changed identically | Keep one copy. | +| Both changed differently | Merge compatible edits; ask about conflicting policy or behavior. | +| Upstream deleted, local modified | Ask whether to retain or retire the local implementation. | +| Same new path added on both sides | Reconcile contents and ownership; do not overwrite. | + +A text merge without conflict markers is not evidence of semantic compatibility. Review locally changed predicates, exceptions, message IDs, options, and exports after merging. Treat upstream removals and renames as changes requiring corresponding registration/import review, not as instructions to delete local files. + +### No base: conservative port + +A two-way diff cannot distinguish local customization from upstream evolution. Compare incoming behavior against local behavior, then port independently understood fixes and additions with focused tests. Preserve unexplained differences. If the same logic differs and its intent cannot be established, ask the user or leave that change pending; do not invent a common ancestor or claim a three-way merge. + +### Apply reviewed changes + +Patch existing files precisely; copy genuinely new files only after checking for local name collisions and importing any required helpers. Preserve local-only rules and existing tests. Keep unresolved changes unapplied and report them explicitly. Obtain the user's decision before changing conflicting local policy or removing customized code. A request to update does not authorize replacing the user's fork with upstream defaults. + +Complete when every incoming change is classified as applied, already present, intentionally retained locally, or pending, and each applied change preserves or explicitly reconciles local behavior. + +## 4. Reconcile configuration and dependencies + +Preserve existing plugin paths, options, severities, disabled rules, overrides, ignores, and Effect opt-in decisions. Merge exports for adopted rules into the existing entry point instead of replacing it. Check native companion rules when adopting related custom rules. + +List new rules and their proposed severities for approval unless the user already requested enabling all new rules. Existing disabled rules stay disabled. Merely adding a new rule implementation need not enable it. + +Check incoming API requirements against the installed `oxlint` and `@oxlint/plugins` versions. Keep a compatible pair and follow repository version policy; change dependencies only when required by the adopted source or requested by the user. Preserve unrelated dependency ranges and the package manager. + +Complete when every adopted rule is reachable through the intended entry point, configuration reflects approved policy, and the dependency pair supports the merged implementation. + +## 5. Verify local and incoming behavior + +Run existing tests and add focused RuleTester cases for changed rule semantics, including regressions that preserve local customizations. Incoming tests are useful evidence, not replacements for local tests. When a skill bundle lacks upstream tests, retrieve relevant tests from the identified upstream revision or write focused cases for the adopted changes. + +Exercise the registered plugin with representative accepted and rejected code. Run the repository's lint, typecheck, and required checks; for Vite+, run `vp check`. Vendored files may be excluded from application lint, so verify changed rules explicitly rather than treating an ignored directory as tested. + +Report application findings separately from plugin/test failures. Application cleanup requires the user's authorization; retain rule severity and safety checks while reporting findings. If a required check is unavailable, name the blocker and mark verification incomplete. + +Complete when adopted behavior and preserved local behavior have test evidence, checks have run, and every failure or verification gap is accounted for. + +## 6. Record the merge and hand back ownership + +Update `UPSTREAM.md` beside the vendored entry point, or the repository's existing provenance record, with: + +- incoming source identity and the recoverable base used, or explicit unknown-base status; +- adopted changes and intentional local deviations; +- deferred/conflicting changes and the decisions still needed; +- dependency/configuration changes and verification results. + +Advance the whole-installation baseline only when the complete incoming snapshot has been reconciled and remaining differences are recorded as intentional local changes. For a partial update, retain the prior baseline and record exactly which changes were adopted or remain pending; do not label the entire tree as matching the new revision. Without a recoverable base, keep that limitation explicit for the next update. + +Review the final diff against the pre-update state. Report applied updates, preserved customizations, pending decisions, checks, and the backup location. Keep backups until the user accepts the result; remove only disposable staging material you created. + +Complete when the user can distinguish upstream updates from local policy, recover the previous state, and identify what a future update must still reconcile. diff --git a/skills/install-anti-slop/scripts/install.mjs b/skills/install-anti-slop/scripts/install.mjs index 0ca7e5b..db1f155 100755 --- a/skills/install-anti-slop/scripts/install.mjs +++ b/skills/install-anti-slop/scripts/install.mjs @@ -5,17 +5,17 @@ import { fileURLToPath } from "node:url"; const skillRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const source = resolve(skillRoot, "assets/anti-slop"); -const args = process.argv.slice(2); -const targetArgument = args.find((arg) => !arg.startsWith("--")); +const arguments_ = process.argv.slice(2); +const targetArgument = arguments_.find((argument) => !argument.startsWith("--")); const target = resolve(process.cwd(), targetArgument ?? "tools/oxlint/anti-slop"); -const force = args.includes("--force"); +const force = arguments_.includes("--force"); if (existsSync(target) && !force) { - console.error(`Refusing to overwrite ${target}. Re-run with --force only after reviewing existing files.`); + console.error(`Refusing to overwrite ${target}. Re-run with --force only after reviewing the existing files.`); process.exit(1); } mkdirSync(dirname(target), { recursive: true }); cpSync(source, target, { recursive: true, force }); -console.log(`Copied anti-slop plugin to ${target}`); +console.log(`Copied the anti-slop plugin to ${target}`); console.log(`Configure Oxlint with: ${target}/index.ts`); diff --git a/skills/install-anti-slop/scripts/manage.mjs b/skills/install-anti-slop/scripts/manage.mjs index 00b5a1c..6aadd80 100755 --- a/skills/install-anti-slop/scripts/manage.mjs +++ b/skills/install-anti-slop/scripts/manage.mjs @@ -1,5 +1,7 @@ #!/usr/bin/env node -import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -14,11 +16,13 @@ const RECOMMENDED_RULES = { }; const STRICT_RULES = { + "anti-slop/no-array-filter-map": "error", "anti-slop/no-chained-type-assertions": "error", "anti-slop/no-conditional-empty-object-spread": "error", "anti-slop/no-known-value-widening": "error", "anti-slop/no-module-mocking": "error", "anti-slop/no-object-parameters": "error", + "anti-slop/no-reduce-accumulator-copy": "error", "anti-slop/no-reflect-apply": "error", "anti-slop/no-reflect-get": "error", "anti-slop/no-runtime-typeof": "error", @@ -28,11 +32,17 @@ const STRICT_RULES = { "anti-slop/no-unknown-type-aliases": "error", "anti-slop/no-unsafe-dictionary-type": "error", "anti-slop/no-widen-then-assert": "error", + "anti-slop/require-readable-spacing": "error", "anti-slop/require-safety-comment-for-type-assertion": "error", + "oxc/no-accumulating-spread": "error", }; const EFFECT_RULES = { + "anti-slop-effect/no-manual-effect-error-tag": "error", + "anti-slop-effect/no-manual-tag-comparison": "error", + "anti-slop-effect/no-manual-tagged-construction": "error", "anti-slop-effect/no-service-constructor-imports": "error", + "anti-slop-effect/prefer-effect-match": "error", }; const DEFAULT_IGNORES = [ @@ -68,6 +78,27 @@ function parseArgs() { return { command, profile, withEffect, force, json, targetDir }; } +function getFiles(dir, base = "") { + let results = []; + if (!existsSync(dir)) return results; + const list = readdirSync(dir, { withFileTypes: true }); + for (const dirent of list) { + const rel = base ? `${base}/${dirent.name}` : dirent.name; + const full = join(dir, dirent.name); + if (dirent.isDirectory()) { + results = results.concat(getFiles(full, rel)); + } else if (dirent.isFile()) { + results.push(rel); + } + } + return results; +} + +function fileHash(path) { + const content = readFileSync(path); + return createHash("sha256").update(content).digest("hex"); +} + function runAudit(cwd, options) { const report = { mode: "audit", @@ -120,7 +151,18 @@ function runInstall(cwd, options) { try { const existing = JSON.parse(readFileSync(configJson, "utf-8")); existing.ignorePatterns = [...new Set([...(existing.ignorePatterns || []), ...ignores])]; - existing.jsPlugins = jsPlugins; + + const existingPlugins = Array.isArray(existing.jsPlugins) ? existing.jsPlugins : []; + const mergedPlugins = [...existingPlugins]; + for (const plugin of jsPlugins) { + const idx = mergedPlugins.findIndex((p) => (typeof p === "string" ? p === plugin.name : p?.name === plugin.name)); + if (idx >= 0) { + mergedPlugins[idx] = plugin; + } else { + mergedPlugins.push(plugin); + } + } + existing.jsPlugins = mergedPlugins; existing.rules = { ...(existing.rules || {}), ...selectedRules }; writeFileSync(configJson, JSON.stringify(existing, null, 2) + "\n", "utf-8"); } catch (e) { @@ -146,6 +188,94 @@ export default defineConfig({ return 0; } +function runUpdate(cwd, options) { + const target = resolve(cwd, options.targetDir); + const relTarget = relative(cwd, target).replace(/\\/g, "/"); + + if (!existsSync(target)) { + console.error(`No existing anti-slop installation found at ${target}. Use install first.`); + return 1; + } + + // Staging for dry-run comparison + const stageDir = mkdtempSync(join(tmpdir(), "anti-slop-update-")); + const stageTarget = join(stageDir, "incoming"); + cpSync(assetSource, stageTarget, { recursive: true }); + + const incomingFiles = new Set(getFiles(stageTarget)); + const liveFiles = new Set(getFiles(target)); + const allRelFiles = Array.from(new Set([...incomingFiles, ...liveFiles])).sort(); + + const classifications = { + add: [], + change: [], + same: [], + localOnly: [], + }; + + for (const rel of allRelFiles) { + const inIncoming = incomingFiles.has(rel); + const inLive = liveFiles.has(rel); + + if (inIncoming && !inLive) { + classifications.add.push(rel); + } else if (!inIncoming && inLive) { + classifications.localOnly.push(rel); + } else { + const incomingHash = fileHash(join(stageTarget, rel)); + const liveHash = fileHash(join(target, rel)); + if (incomingHash === liveHash) { + classifications.same.push(rel); + } else { + classifications.change.push(rel); + } + } + } + + rmSync(stageDir, { recursive: true, force: true }); + + if (!options.force) { + const summary = { + add: classifications.add.length, + change: classifications.change.length, + same: classifications.same.length, + localOnly: classifications.localOnly.length, + }; + + if (options.json) { + console.log( + JSON.stringify( + { + mode: "update", + target: relTarget, + dryRun: true, + mutations: 0, + classifications, + summary, + }, + null, + 2 + ) + ); + } else { + console.log(`=== Anti-Slop Update Review (Dry-run: no files modified) ===`); + console.log(`Target: ${relTarget}`); + for (const f of classifications.add) console.log(` [ADD] ${f}`); + for (const f of classifications.change) console.log(` [CHANGE] ${f}`); + for (const f of classifications.localOnly) console.log(` [LOCAL-ONLY] ${f}`); + for (const f of classifications.same) console.log(` [SAME] ${f}`); + console.log( + `Summary: ${summary.add} to add, ${summary.change} to change, ${summary.same} unchanged, ${summary.localOnly} local-only.` + ); + console.log("No modifications made to repository (reviewed merge doctrine)."); + console.log("To apply changes and overwrite live files, re-run with --force."); + } + return 0; + } + + return runInstall(cwd, { ...options, force: true }); +} + function runRemove(cwd, options) { const target = resolve(cwd, options.targetDir); let removedCount = 0; @@ -174,7 +304,7 @@ function main() { } else if (options.command === "install") { process.exit(runInstall(cwd, options)); } else if (options.command === "update") { - process.exit(runInstall(cwd, { ...options, force: true })); + process.exit(runUpdate(cwd, options)); } else if (options.command === "remove") { process.exit(runRemove(cwd, options)); } else { From 4cfb99ce28783d75514f56b0f44ada5103c3329e Mon Sep 17 00:00:00 2001 From: Fahmi Harun <34875577+kuker24@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:59:51 +0700 Subject: [PATCH 3/4] docs: notices/provenance for c44ef22 + eslint-stylistic --- THIRD_PARTY_NOTICES.md | 3 ++- docs/source-wave.md | 2 +- skills/install-anti-slop/NOTICE.md | 26 +++++++++++++++++------- vendor/license-audit.json | 2 +- vendor/licenses/ESLINT-STYLISTIC-MIT.txt | 22 ++++++++++++++++++++ vendor/provenance.json | 8 ++++---- vendor/sources.json | 15 +++++++++++--- 7 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 vendor/licenses/ESLINT-STYLISTIC-MIT.txt diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index df984fc..1c57263 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -23,7 +23,8 @@ Machine-readable copy: `vendor/license-audit.json`. | `scroll-craft` | [nateherkai/scroll-craft](https://github.com/nateherkai/scroll-craft) `75d81f7` — `vendor/licenses/NATEHERK-SCROLL-CRAFT-MIT.txt`; skill `NOTICE.md` | MIT © 2026 Nate Herk | follow MIT | | `playwright-qa` | [microsoft/playwright-cli](https://github.com/microsoft/playwright-cli) `b85c7a7` — `vendor/licenses/MICROSOFT-PLAYWRIGHT-CLI-APACHE2.txt`; skill `NOTICE.md` | Apache-2.0 © Microsoft Corporation | follow Apache-2.0 | | `taste-guard` | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) `ccbc156` — `vendor/licenses/LEONXLNX-TASTE-MIT.txt`; integrated in Impeccable | MIT © 2026 Leonxlnx | follow MIT | -| `install-anti-slop` | [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) `e8c4880` — `vendor/licenses/DMMULROY-ANTI-SLOP-MIT.txt`; skill `NOTICE.md` (updated with `update` mode) | MIT © 2026 Dillon Mulroy | follow MIT | +| `install-anti-slop` | [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) `c44ef22` — `vendor/licenses/DMMULROY-ANTI-SLOP-MIT.txt`; skill `NOTICE.md` (updated with `update` mode) | MIT © 2026 Dillon Mulroy | follow MIT | +| `eslint-stylistic` (in `install-anti-slop`) | [eslint-stylistic/eslint-stylistic](https://github.com/eslint-stylistic/eslint-stylistic) `435c3ea` (v6.0.0-beta.6) — `vendor/licenses/ESLINT-STYLISTIC-MIT.txt` | MIT © OpenJS Foundation and contributors; © 2023-PRESENT ESLint Stylistic contributors | follow MIT | | `humanizer` | [blader/humanizer](https://github.com/blader/humanizer) 3.1.0 (`225a6f3`); skill `NOTICE.md` | MIT © 2024-2026 blader contributors | follow MIT | | `academic` | Original first-party text. Conceptual pipeline (research→write→review→revise) independently implemented. No source copied from Imbad0202/academic-research-skills (CC-BY-NC-4.0). | MIT © 2026 OpenCodeHighEnd contributors | follow MIT | | `hyperframes` | [heygen-com/hyperframes](https://github.com/heygen-com/hyperframes) v0.8.119 (`3a0299e`), refreshed in 0.1.13 to v0.8.122 (`6037d22`); skill `NOTICE.md` | Apache-2.0 | follow Apache-2.0 | diff --git a/docs/source-wave.md b/docs/source-wave.md index 3f66a5c..43fddcb 100644 --- a/docs/source-wave.md +++ b/docs/source-wave.md @@ -6,7 +6,7 @@ Recorded per Phase 0 contract. | Source | Upstream Commit / Ref | Nature / Contents | Disposition | Survivor in OCBF | Notes / Rationale | |---|---|---|:---:|---|---| | [miqdadbadjuber/anti-slop](https://github.com/miqdadbadjuber/anti-slop) | `91f12ec67e9de6043cfd93b846404986ba73c3f4` (v3.2.20) | UI/copy filter (38 rules R-01–R-38), 3 tiers (Hard Gate, Purpose-Gate, Quality Locks), Delivery Gate checklist, Liveliness dials, during/after usage modes. MIT. | **MERGE** | `skills/impeccable` (taste-guard + direction), `skills/humanizer`, `rules/03-prose-discipline.md` | Filter, not a style guide. Do not vendor as 65th skill (`antislop` or `antislop-ui`). Distinct from Oxlint. | -| [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) | `e8c4880471b23ab7f216fba7b27d173a6ef07d4c` (v0.1.2) | TypeScript/JavaScript Oxlint static linter ruleset. MIT. | **DONE** | `skills/install-anti-slop` | Vendored and pinned at v0.1.2. 23 assets blob-identical to e8c4880 since 0.1.15 (restored local drift in rules/no-widen-then-assert.ts from 0.1.0 bootstrap; upstream was correct). update mode is first-party OCH. c44ef22 evaluated: 3 generic rules, 4 Effect rules, shared helpers, vendored eslint-stylistic (MIT) DEFERRED (not vendored). | +| [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) | `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b` (0.1.2+c44ef22) | TypeScript/JavaScript Oxlint static linter ruleset. MIT. | **SYNCED** | `skills/install-anti-slop` | 38 assets blob-identical to c44ef22 (untagged main, post-v0.1.2); includes vendored eslint-stylistic (MIT) adapted upstream; update mode reconciled with upstream references/update.md. | | [microsoft/markitdown](https://github.com/microsoft/markitdown) | `b8f79c57ebc0044be41323d89b2a45d3fda8460e` (v0.1.8) | File to Markdown converter (Office/PDF/HTML/CSV/XLSX/PPTX/EPUB/ZIP). MIT. | **PIN_ONLY** | `skills/markitdown` | Pinned to v0.1.8 (commit `b8f79c57`). Skill body unchanged. Enable writes `uvx --from markitdown-mcp==0.0.1a7 --with markitdown[all]==0.1.8 markitdown-mcp`. Output remains data-only. SmartDoc keeps contract/QA/render. MCP remains FOREIGN_ON_DEMAND. | | [affaan-m/ECC](https://github.com/affaan-m/ECC) | `dd6ee538aee0f548d4a6b520118f875431fd749e` | External agent control plane (68 agents, 292 skills, hooks, learning runtime). | **REJECT** | None (`FOREIGN_ON_DEMAND`) | Do not vendor harness control plane or 292 skills. No installer mutator. Doctor does not fail when absent. Individual warehouse ports remain first-party MIT. | | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) | `e79ca9ec7e071eb3a3b623c4fb752e853fc3ed58` (`ccbc156` base) | Design taste dials (VARIANCE, MOTION, DENSITY), quality rules, GSAP/Tailwind references. MIT. | **MERGE** | `skills/impeccable/reference/taste/direction.md`, `taste-guard.md` | Dials already integrated into Impeccable surface brief. Fenced after Design Bank or DESIGN.md direction exists. Never a frontend-design twin. | diff --git a/skills/install-anti-slop/NOTICE.md b/skills/install-anti-slop/NOTICE.md index 9b5a433..3e1dd7d 100644 --- a/skills/install-anti-slop/NOTICE.md +++ b/skills/install-anti-slop/NOTICE.md @@ -3,21 +3,33 @@ This skill vendors and adapts the Anti-Slop Oxlint plugin originally authored by Dillon Mulroy. - Upstream repository: https://github.com/dmmulroy/anti-slop -- Upstream commit: e8c4880471b23ab7f216fba7b27d173a6ef07d4c -- Upstream version: 0.1.2 +- Upstream commit: c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b +- Upstream version: 0.1.2+c44ef22 (untagged main, post-v0.1.2) - License: MIT License (see vendor/licenses/DMMULROY-ANTI-SLOP-MIT.txt) - Copyright (c) 2026 Dillon Mulroy +## Bundled Third-Party Code +- **eslint-stylistic**: Vendored under `assets/anti-slop/vendor/eslint-stylistic/`. + - Upstream source: https://github.com/eslint-stylistic/eslint-stylistic + - Commit: `435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08` (tag `v6.0.0-beta.6`) + - License: MIT License (see `vendor/licenses/ESLINT-STYLISTIC-MIT.txt` and `assets/anti-slop/vendor/eslint-stylistic/LICENSE`) + - Copyright OpenJS Foundation and other contributors, + - Copyright (c) 2023-PRESENT ESLint Stylistic contributors + - Note: Adapted and ported to Oxlint AST types by Dillon Mulroy upstream (see `assets/anti-slop/vendor/eslint-stylistic/UPSTREAM.md`), not by OpenCodeHighEnd. + ## Modifications for OpenCodeHighEnd +- Vendored 38 assets under `assets/anti-slop/` are byte-identical to upstream `c44ef22` (zero local modifications to linter rule assets). - Adapted as an opt-in model-invoked skill for TypeScript/JavaScript projects. - Added `scripts/manage.mjs` supporting 5 explicit modes: - `audit`: isolated discovery reporting findings per rule and file category without repo mutations. - - `recommended`: curated high-signal OCBF profile (`no-chained-type-assertions`, `no-widen-then-assert`, audit on `no-known-value-widening`, audit/warn on `require-safety-comment-for-type-assertion`). - - `strict`: full 15-rule generic ruleset from upstream snapshot. + - `recommended`: curated high-signal profile (`no-chained-type-assertions`, `no-widen-then-assert`, audit on `no-known-value-widening`, audit/warn on `require-safety-comment-for-type-assertion`). + - `strict`: full 18-rule generic ruleset from upstream snapshot plus native companion `oxc/no-accumulating-spread`. - `custom`: project-configured rules. - - `update`: refreshes vendored rules and assets while preserving current profile preferences. - - `effect`: opt-in Effect service layer rules for direct Effect dependencies. -- Added safe removal, update, idempotency checks, and collision detection. + - `update`: non-destructive dry-run comparison (`[ADD]`, `[CHANGE]`, `[SAME]`, `[LOCAL-ONLY]`) following upstream `references/update.md` merge doctrine; mutations applied only with `--force`. + - `effect`: 5 opt-in Effect architectural rules for direct Effect dependencies. +- Added safe removal, non-destructive update, idempotency checks, and collision detection. +- Restored `scripts/install.mjs` to byte-identical upstream snapshot (`db1f155b`). +- Added `references/update.md` byte-identical to upstream snapshot (`b2e7f675`). - Strictly segregated from core OCBF Python dependencies (no Oxlint forced onto OCBF itself). ## Additional Attribution diff --git a/vendor/license-audit.json b/vendor/license-audit.json index 6c6cb86..ba13953 100644 --- a/vendor/license-audit.json +++ b/vendor/license-audit.json @@ -216,7 +216,7 @@ }, "install-anti-slop": { "license": "MIT", - "evidence": "dmmulroy/anti-slop e8c4880 + vendor/licenses/DMMULROY-ANTI-SLOP-MIT.txt; NOTICE.md", + "evidence": "dmmulroy/anti-slop c44ef22 + vendor/licenses/DMMULROY-ANTI-SLOP-MIT.txt + vendor/licenses/ESLINT-STYLISTIC-MIT.txt; NOTICE.md", "redistribution": "mit" }, "playwright-qa": { diff --git a/vendor/licenses/ESLINT-STYLISTIC-MIT.txt b/vendor/licenses/ESLINT-STYLISTIC-MIT.txt new file mode 100644 index 0000000..38dbc35 --- /dev/null +++ b/vendor/licenses/ESLINT-STYLISTIC-MIT.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright OpenJS Foundation and other contributors, +Copyright (c) 2023-PRESENT ESLint Stylistic contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/provenance.json b/vendor/provenance.json index 2ce7e2c..96308ce 100644 --- a/vendor/provenance.json +++ b/vendor/provenance.json @@ -498,13 +498,13 @@ "component": "install-anti-slop", "path": "skills/install-anti-slop", "upstream": "https://github.com/dmmulroy/anti-slop", - "commit": "e8c4880471b23ab7f216fba7b27d173a6ef07d4c", - "version": "0.1.2", + "commit": "c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b", + "version": "0.1.2+c44ef22", "license": "MIT", - "copyright": "Copyright (c) 2026 Dillon Mulroy", + "copyright": "Copyright (c) 2026 Dillon Mulroy; OpenJS Foundation / ESLint Stylistic contributors", "modified": true, "redistribution": "mit", - "notes": "Vendored rule snapshot with 5 modes (audit, recommended, strict, custom, update) and opt-in Effect rules." + "notes": "Vendored rule snapshot (38 assets) with 5 modes (audit, recommended, strict, custom, update), 18 generic rules, 5 opt-in Effect rules, and upstream-adapted eslint-stylistic padding rules." }, { "component": "taste-guard", diff --git a/vendor/sources.json b/vendor/sources.json index a1f47b3..b8614e4 100644 --- a/vendor/sources.json +++ b/vendor/sources.json @@ -219,12 +219,21 @@ }, "anti-slop": { "repository": "https://github.com/dmmulroy/anti-slop", - "commit": "e8c4880471b23ab7f216fba7b27d173a6ef07d4c", - "version": "0.1.2", + "commit": "c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b", + "version": "0.1.2+c44ef22", "license": "MIT", "copyright": "Copyright (c) 2026 Dillon Mulroy", "licenseFile": "vendor/licenses/DMMULROY-ANTI-SLOP-MIT.txt", - "note": "Vendored rule snapshot for TS/JS projects with 5 modes (audit, recommended, strict, custom, update) and opt-in Effect rules." + "note": "Vendored rule snapshot for TS/JS projects with 5 modes (audit, recommended, strict, custom, update), 18 generic rules, and 5 opt-in Effect rules." + }, + "eslint-stylistic": { + "repository": "https://github.com/eslint-stylistic/eslint-stylistic", + "commit": "435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08", + "version": "v6.0.0-beta.6", + "license": "MIT", + "copyright": "Copyright OpenJS Foundation and other contributors, ; Copyright (c) 2023-PRESENT ESLint Stylistic contributors", + "licenseFile": "vendor/licenses/ESLINT-STYLISTIC-MIT.txt", + "note": "Vendored padding-line rules adapted upstream by Dillon Mulroy for Oxlint AST." }, "json-render": { "repository": "https://github.com/vercel-labs/json-render", From 6e005794d498c7c7c938d09bbd5d503b4bb4c30e Mon Sep 17 00:00:00 2001 From: Fahmi Harun <34875577+kuker24@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:03:38 +0700 Subject: [PATCH 4/4] chore(release): 0.1.17 --- CHANGELOG.md | 17 +++++ README.md | 2 +- VERSION | 2 +- docs/CATALOG-FREEZE.md | 3 +- tests/test_anti_slop.py | 126 +++++++++++++++++++++++++++++++------- vendor/license-audit.json | 2 +- vendor/provenance.json | 2 +- vendor/sources.json | 2 +- 8 files changed, 127 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 743e6d4..c54a76c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.1.17 — 2026-10-04 + +Rilis sinkronisasi Wave 0.1.17 untuk pembaruan penuh aset anti-slop ke commit HEAD upstream `dmmulroy/anti-slop@c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b` (label versi `0.1.2+c44ef22 (untagged main, post-v0.1.2)`). Katalog tetap 65 (50 model + 15 manual). Closed intents tetap 25. Tidak ada penambahan atau pensiun skill (`vendor/skill-allowlist.txt` dan `vendor/skill-policy.json` tidak berubah). + +- **Sinkronisasi penuh aset anti-slop (c44ef22)**: Menyinkronkan seluruh 38 berkas aset linter Oxlint di `skills/install-anti-slop/assets/anti-slop` secara byte-identik dari upstream snapshot `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b` (15 berkas baru, 5 berkas diperbarui, 18 berkas identik). Termasuk penambahan 3 aturan generik baru (`no-array-filter-map`, `no-reduce-accumulator-copy`, `require-readable-spacing`), 4 aturan Effect baru (`no-manual-effect-error-tag`, `no-manual-tag-comparison`, `no-manual-tagged-construction`, `prefer-effect-match`), modul pembantu `shared/array-method.ts` dan `shared/scope.ts`, serta adaptasi upstream dari `eslint-stylistic` (tag `v6.0.0-beta.6`, MIT). +- **Rekonsiliasi profil dan aturan**: + - Profil `recommended` dipertahankan pada 4 aturan asersi keamanan tipe berkekuatan sinyal tinggi (`no-chained-type-assertions`, `no-widen-then-assert`, `no-known-value-widening`, `require-safety-comment-for-type-assertion`), tanpa memasukkan `require-readable-spacing` demi menghindari bentrok dengan formatter eksternal. + - Profil `strict` diperluas menjadi 18 aturan generik plus aturan bawaan companion `"oxc/no-accumulating-spread": "error"`. + - Grup aturan `effect` diperluas menjadi 5 aturan arsitektural Effect. +- **Mode update non-destruktif dan perbaikan skrip**: + - Mode `update` pada `scripts/manage.mjs` diubah menjadi non-destruktif mengikuti doktrin upstream `references/update.md`: melakukan perbandingan staged dry-run dan mencetak klasifikasi per berkas (`[ADD]`, `[CHANGE]`, `[SAME]`, `[LOCAL-ONLY]`) tanpa memutasi repositori, serta mewajibkan bendera `--force` untuk menerapkan perubahan. + - Penggabungan konfigurasi `jsPlugins` pada `.oxlintrc.json` kini dilakukan per entri `name` guna mencegah penimpaan plugin pihak ketiga yang sudah ada. + - Berkas `scripts/install.mjs` dikembalikan ke status byte-identik upstream (`db1f155bd15c065ddb6024042dea7ca4992551b2`). + - Berkas panduan `references/update.md` disertakan byte-identik dari upstream (`b2e7f6751a7a97406b768ed5bf950d883c17e9a6`). +- **Atribusi lisensi dan provenance**: Menambahkan salinan lisensi MIT untuk `eslint-stylistic` di `vendor/licenses/ESLINT-STYLISTIC-MIT.txt`, memperbarui `skills/install-anti-slop/NOTICE.md`, `THIRD_PARTY_NOTICES.md`, `vendor/sources.json`, `vendor/provenance.json`, `vendor/license-audit.json`, serta mengubah disposisi `anti-slop` di `docs/source-wave.md` menjadi `**SYNCED**`. +- **Versi Produk**: Bump versi ke `0.1.17` (`VERSION`, `vendor/sources.json`, `vendor/provenance.json`, `vendor/license-audit.json`, `README.md`, `docs/CATALOG-FREEZE.md`). + ## 0.1.16 — 2026-10-04 Patch koreksi pasca-0.1.15 untuk atribusi aset anti-slop, pemulihan catatan penundaan upstream c44ef22, dan penyempurnaan emulasi Playwright QA (permissions geolokasi). Katalog tetap 65 (50 model + 15 manual). Closed intents tetap 25. Tidak ada penambahan atau pensiun skill (`vendor/skill-allowlist.txt` dan `vendor/skill-policy.json` tidak berubah). diff --git a/README.md b/README.md index 44f6d94..92cf44f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ OpenCode 2 overlay: 65 frozen routed skills, thin `AGENTS.md`, `opencode-he`. Installer and runtime overlay for [OpenCode 2](https://opencode.ai/v2/docs/). -Version **0.1.16**. The 65-skill catalog is strictly frozen. +Version **0.1.17**. The 65-skill catalog is strictly frozen. ## What it is diff --git a/VERSION b/VERSION index e8e277f..04c5555 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.16 +0.1.17 diff --git a/docs/CATALOG-FREEZE.md b/docs/CATALOG-FREEZE.md index db00978..2e3c505 100644 --- a/docs/CATALOG-FREEZE.md +++ b/docs/CATALOG-FREEZE.md @@ -2,8 +2,9 @@ This contract defines the immutable boundary and governance for the OpenCodeHighEnd catalog. The 65-skill catalog is strictly frozen. -- **Product version**: 0.1.16 +- **Product version**: 0.1.17 - **Catalog**: 65 names. 50 model-invoked under `skills/`. 15 manual under `manual-skills/` + `commands/`. +- **Wave 0.1.17 body-only**: catalog strictly frozen at 65 (50 model + 15 manual). Zero catalog growth. No new exceptions. Upstream anti-slop full synchronization to `dmmulroy/anti-slop@c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b` (version `0.1.2+c44ef22`): vendored 38 assets byte-identical to upstream tree (18 generic rules, 5 Effect rules, shared helpers, and upstream-adapted eslint-stylistic padding rules under MIT); reconciled profiles (recommended stays 4 high-signal assertions; strict expanded to 18 generic rules plus companion native `oxc/no-accumulating-spread`; effect expanded to 5 rules); converted `manage.mjs update` to non-destructive dry-run review per upstream `references/update.md` merge doctrine (requiring `--force` to mutate live files) with per-name `jsPlugins` merging; restored `scripts/install.mjs` byte-identical to upstream (`db1f155b`); added `vendor/licenses/ESLINT-STYLISTIC-MIT.txt` and comprehensive notices. - **Wave 0.1.16 body-only**: catalog strictly frozen at 65 (50 model + 15 manual). Zero catalog growth. No new exceptions. Post-release correction and Playwright emulation refinement: restored c44ef22 upstream deferral record in docs/source-wave.md and corrected attribution of anti-slop asset synchronization (restoring 0.1.0 local drift to upstream e8c4880, not fixing upstream); documented mandatory permissions array for Playwright QA geolocation emulation and clarified runtime split (cross-browser grantPermissions/setGeolocation vs Chromium CDP timezone/locale overrides). - **Wave 0.1.15 body-only**: catalog strictly frozen at 65 (50 model + 15 manual). Zero catalog growth. No new exceptions. Upstream sync and hygiene: synchronized vendored Oxlint ruleset assets in `skills/install-anti-slop` from `dmmulroy/anti-slop@e8c4880471b23ab7f216fba7b27d173a6ef07d4c` (v0.1.2) restoring upstream no-widen-then-assert.ts (local drift since 0.1.0); standardized Playwright QA timezone/locale/geolocation emulation doctrine with `.playwright/cli.config.json` context options as primary cross-browser method and `run-code` as Chromium CDP runtime alternative; removed dead imports `repo_root` from `lib/design_v2/bootstrap.py` and `tarfile` from `tests/test_design_bootstrap.py`. - **Wave 0.1.14 body-only**: catalog strictly frozen at 65 (50 model + 15 manual). Zero catalog growth. No new exceptions. Post-release fixes for 0.1.13: corrected crawl4ai commit pin in `vendor/sources.json` to official `v0.9.4` release (`133e1d92e37885dfccc03ea2e3687d06c98b7ceb`); completed retirement of Design Bank fallback #4 from runtime and tests with fail-closed Drive error propagation; corrected Playwright CLI config path documentation (`.playwright/cli.config.json`) and clarified timezone/locale emulation via `run-code`; aligned reflect invariant enforcement hierarchy wording (5-level OCH adaptation). diff --git a/tests/test_anti_slop.py b/tests/test_anti_slop.py index 62cb255..ae2baff 100644 --- a/tests/test_anti_slop.py +++ b/tests/test_anti_slop.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 from __future__ import annotations +import hashlib import json import re -import shutil import subprocess import tempfile import unittest @@ -17,12 +17,15 @@ SKILL = ROOT / "skills" / "install-anti-slop" FM = re.compile(r"\A---\n(.*?\n)---\n", re.DOTALL) + EXPECTED_RULES = ( + "no-array-filter-map.ts", "no-chained-type-assertions.ts", "no-conditional-empty-object-spread.ts", "no-known-value-widening.ts", "no-module-mocking.ts", "no-object-parameters.ts", + "no-reduce-accumulator-copy.ts", "no-reflect-apply.ts", "no-reflect-get.ts", "no-runtime-typeof.ts", @@ -32,9 +35,23 @@ "no-unknown-type-aliases.ts", "no-unsafe-dictionary-type.ts", "no-widen-then-assert.ts", + "require-readable-spacing.ts", "require-safety-comment-for-type-assertion.ts", ) +EXPECTED_EFFECT_RULES = ( + "no-manual-effect-error-tag.ts", + "no-manual-tag-comparison.ts", + "no-manual-tagged-construction.ts", + "no-service-constructor-imports.ts", + "prefer-effect-match.ts", +) + + +def git_blob_sha(data: bytes) -> str: + header = f"blob {len(data)}\0".encode("utf-8") + return hashlib.sha1(header + data).hexdigest() + class AntiSlopContractTests(unittest.TestCase): def test_policy_model_invoked(self): @@ -58,23 +75,37 @@ def test_frontmatter_and_separation_from_unslop(self): self.assertIn("prose editing (use /unslop)", block) self.assertLessEqual(text.count("\n"), 200) - def test_vendored_rule_snapshot_exists(self): + def test_blob_identity_manifest(self): + manifest_path = ROOT / "tests" / "fixtures" / "anti-slop-c44ef22-manifest.json" + self.assertTrue(manifest_path.is_file()) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + self.assertEqual(len(manifest), 38) + assets = SKILL / "assets" / "anti-slop" + live_files = {p.relative_to(assets).as_posix() for p in assets.rglob("*") if p.is_file()} + self.assertEqual(live_files, set(manifest.keys())) + for rel_path, expected_sha in manifest.items(): + file_path = assets / rel_path + computed_sha = git_blob_sha(file_path.read_bytes()) + self.assertEqual(computed_sha, expected_sha, f"Blob mismatch for {rel_path}") + + def test_rule_counts_and_registrations(self): assets = SKILL / "assets" / "anti-slop" - self.assertTrue((assets / "index.ts").is_file()) - self.assertTrue((assets / "effect" / "index.ts").is_file()) - self.assertTrue((assets / "effect" / "rules" / "no-service-constructor-imports.ts").is_file()) - for rule_file in EXPECTED_RULES: - path = assets / "rules" / rule_file - self.assertTrue(path.is_file(), rule_file) - shared = assets / "shared" - for shared_file in ( - "dictionary-types.ts", - "function-parameters.ts", - "type-alias-resolution.ts", - "lexical-type-parameters.ts", - "reflect-method.ts", - ): - self.assertTrue((shared / shared_file).is_file(), shared_file) + rule_files = sorted(p.name for p in (assets / "rules").glob("*.ts")) + self.assertEqual(len(rule_files), 18) + self.assertEqual(tuple(rule_files), EXPECTED_RULES) + + effect_files = sorted(p.name for p in (assets / "effect" / "rules").glob("*.ts")) + self.assertEqual(len(effect_files), 5) + self.assertEqual(tuple(effect_files), EXPECTED_EFFECT_RULES) + + index_ts = (assets / "index.ts").read_text(encoding="utf-8") + effect_index_ts = (assets / "effect" / "index.ts").read_text(encoding="utf-8") + for r in EXPECTED_RULES: + name = r[:-3] + self.assertIn(f'"{name}":', index_ts) + for r in EXPECTED_EFFECT_RULES: + name = r[:-3] + self.assertIn(f'"{name}":', effect_index_ts) def test_license_and_sources_inventory(self): audit = json.loads((ROOT / "vendor" / "license-audit.json").read_text(encoding="utf-8")) @@ -84,12 +115,38 @@ def test_license_and_sources_inventory(self): self.assertTrue(lic.is_file()) text = lic.read_text(encoding="utf-8") self.assertIn("Copyright (c) 2026 Dillon Mulroy", text) + + # eslint-stylistic license checks + stylistic_lic = ROOT / "vendor" / "licenses" / "ESLINT-STYLISTIC-MIT.txt" + self.assertTrue(stylistic_lic.is_file()) + st_text = stylistic_lic.read_text(encoding="utf-8") + self.assertIn("Copyright OpenJS Foundation and other contributors", st_text) + self.assertIn("Copyright (c) 2023-PRESENT ESLint Stylistic contributors", st_text) + + upstream_lic = SKILL / "assets" / "anti-slop" / "vendor" / "eslint-stylistic" / "LICENSE" + self.assertTrue(upstream_lic.is_file()) + self.assertEqual(stylistic_lic.read_text(encoding="utf-8"), upstream_lic.read_text(encoding="utf-8")) + + tp = (ROOT / "THIRD_PARTY_NOTICES.md").read_text(encoding="utf-8") + self.assertIn("eslint-stylistic", tp) + sources = json.loads((ROOT / "vendor" / "sources.json").read_text(encoding="utf-8")) self.assertEqual( sources["sources"]["anti-slop"]["commit"], - "e8c4880471b23ab7f216fba7b27d173a6ef07d4c", + "c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b", + ) + self.assertEqual(sources["sources"]["anti-slop"]["version"], "0.1.2+c44ef22") + self.assertEqual( + sources["sources"]["eslint-stylistic"]["commit"], + "435c3ea0fd26a5fef9042c4b36b6e165fbbf8d08", ) - self.assertEqual(sources["sources"]["anti-slop"]["version"], "0.1.2") + + def test_install_and_update_reference_blobs(self): + install_mjs = SKILL / "scripts" / "install.mjs" + self.assertEqual(git_blob_sha(install_mjs.read_bytes()), "db1f155bd15c065ddb6024042dea7ca4992551b2") + + update_md = SKILL / "references" / "update.md" + self.assertEqual(git_blob_sha(update_md.read_bytes()), "b2e7f6751a7a97406b768ed5bf950d883c17e9a6") def test_manage_script_audit_mode_zero_mutations(self): with tempfile.TemporaryDirectory() as tmpdir: @@ -107,7 +164,7 @@ def test_manage_script_audit_mode_zero_mutations(self): self.assertTrue(data["clean"]) self.assertEqual(list(Path(tmpdir).iterdir()), []) - def test_manage_script_install_recommended_and_remove(self): + def test_manage_script_install_recommended_update_and_remove(self): with tempfile.TemporaryDirectory() as tmpdir: manage_script = SKILL / "scripts" / "manage.mjs" # 1. Install recommended @@ -137,9 +194,24 @@ def test_manage_script_install_recommended_and_remove(self): self.assertNotEqual(res_refuse.returncode, 0) self.assertIn("Refusing to overwrite", res_refuse.stderr) - # 2b. Update succeeds by overwriting with current preferences + # 3. Update without --force is non-destructive dry-run (zero mutations) + mtime_before = copied_entry.stat().st_mtime_ns + res_dry_update = subprocess.run( + ["node", str(manage_script), "update", "--json"], + cwd=tmpdir, + capture_output=True, + text=True, + check=True, + ) + dry_data = json.loads(res_dry_update.stdout) + self.assertEqual(dry_data["mode"], "update") + self.assertTrue(dry_data["dryRun"]) + self.assertEqual(dry_data["mutations"], 0) + self.assertEqual(copied_entry.stat().st_mtime_ns, mtime_before) + + # 4. Update with --force succeeds and overwrites/applies res_update = subprocess.run( - ["node", str(manage_script), "update", "--profile", "recommended"], + ["node", str(manage_script), "update", "--force", "--profile", "recommended"], cwd=tmpdir, capture_output=True, text=True, @@ -147,7 +219,7 @@ def test_manage_script_install_recommended_and_remove(self): ) self.assertIn("Installed anti-slop plugin (recommended)", res_update.stdout) - # 3. Remove + # 5. Remove res_remove = subprocess.run( ["node", str(manage_script), "remove"], cwd=tmpdir, @@ -173,7 +245,15 @@ def test_manage_script_strict_and_effect_options(self): config_ts = Path(tmpdir) / "oxlint.config.ts" content = config_ts.read_text(encoding="utf-8") self.assertIn("anti-slop/no-module-mocking", content) + self.assertIn("anti-slop/no-array-filter-map", content) + self.assertIn("anti-slop/no-reduce-accumulator-copy", content) + self.assertIn("anti-slop/require-readable-spacing", content) + self.assertIn("oxc/no-accumulating-spread", content) self.assertIn("anti-slop-effect/no-service-constructor-imports", content) + self.assertIn("anti-slop-effect/no-manual-effect-error-tag", content) + self.assertIn("anti-slop-effect/no-manual-tag-comparison", content) + self.assertIn("anti-slop-effect/no-manual-tagged-construction", content) + self.assertIn("anti-slop-effect/prefer-effect-match", content) if __name__ == "__main__": diff --git a/vendor/license-audit.json b/vendor/license-audit.json index ba13953..c5a3f27 100644 --- a/vendor/license-audit.json +++ b/vendor/license-audit.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "product": "OpenCodeHighEnd", - "productVersion": "0.1.16", + "productVersion": "0.1.17", "note": "Evidence-based. A missing frontmatter license is not a grant. Adapted \u2260 first-party.", "skills": { "demo-video": { diff --git a/vendor/provenance.json b/vendor/provenance.json index 96308ce..ce4ca84 100644 --- a/vendor/provenance.json +++ b/vendor/provenance.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "product": "OpenCodeHighEnd", - "productVersion": "0.1.16", + "productVersion": "0.1.17", "firstPartyLicense": "MIT", "components": [ { diff --git a/vendor/sources.json b/vendor/sources.json index b8614e4..f1dbb13 100644 --- a/vendor/sources.json +++ b/vendor/sources.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "product": "OpenCodeHighEnd", - "productVersion": "0.1.16", + "productVersion": "0.1.17", "sources": { "codebase-memory": { "repository": "https://github.com/DeusData/codebase-memory-mcp",