@deslop/coding-standards 0.1.761 → 0.1.785

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Coding standards
2
+
3
+ Strict Oxlint preset and repository skill installer. Requires Node 26+; skill installation runs inside a Git repository, including linked worktrees.
4
+
5
+ ## Install skills
6
+
7
+ ```bash
8
+ vpx @deslop/coding-standards@latest
9
+ ```
10
+
11
+ | Result | Scope |
12
+ | -------------------------------------- | -------------------------------------------------------------------------------------------------- |
13
+ | Engineering, design and testing skills | Git root's existing `.agents`, `.claude` and `.codex` directories; creates `.agents` if none exist |
14
+ | Refresh | Replaces only those three skill names; preserves other skills and global configuration |
15
+ | Boundary | Rejects destinations escaping the repository or overlapping global configuration |
16
+
17
+ No repository dependency, manifest/lock change or lint configuration change. Treat installed skill copies as read-only; refresh through the CLI.
18
+
19
+ ## Use the lint preset
20
+
21
+ ```bash
22
+ vp add -D @deslop/coding-standards @effect/tsgo
23
+ vp exec effect-tsgo patch --no-typescript --oxlint
24
+ ```
25
+
26
+ ```ts
27
+ import {defineConfig} from 'vite-plus'
28
+ import {oxlint} from '@deslop/coding-standards'
29
+
30
+ export default defineConfig({lint: {extends: [oxlint]}})
31
+ ```
32
+
33
+ The preset uses Effect-tsgo's patched Oxlint and compatible tsgolint; keep the patch command in the project's prepare script. Match upstream supported tool versions. The skill installer neither configures lint nor upgrades/patches its toolchain.
34
+
35
+ ## Working here
36
+
37
+ | Owner | Decision |
38
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39
+ | `src/` | Flat package structure; rules in `src/rules`, no separate plugin or install-test harness |
40
+ | `skills/` | Maintained engineering/design/testing sources; refresh repository copies from the TypeScript CLI, no build needed |
41
+ | Lint rules | Prefer an equivalent maintained rule before custom enforcement; consumer code conforms to strict standards, not the reverse. Fix broken/imprecise rules, not inconvenient valid diagnostics |
42
+ | Rule proof | Existing public CLI regression cases, with accepted/rejected controls; no severity/exclusion workaround |
43
+
44
+ ## Rejected shortcuts
45
+
46
+ | Candidate | Reason |
47
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
48
+ | Blanket `unknown` type ban | Opaque SDK/framework contracts legitimately need it; narrow/decode before use instead of fabricating a shape |
49
+ | Functional preset or dependency just to replace `no-let` | The preset conflicts; a selective plugin adds dependencies for a tiny rule |
50
+ | Syntax-only generator-failure rule | Factory-call syntax does not establish yieldability; the existing type-aware Effect rule owns that check |
51
+
52
+ With workspace dependencies installed, from the repository root:
53
+
54
+ ```bash
55
+ vp test run tools/coding-standards/src/rules/rules.test.ts
56
+ node tools/coding-standards/src/install.ts
57
+ ```
58
+
59
+ ## Skill inspiration
60
+
61
+ | Source | Used / boundary |
62
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | [Matt Pocock's engineering skills](https://github.com/mattpocock/skills/tree/d81f3a183412e71a5b1e84ca21bc1a35eea03a60/skills/engineering) | Caller-first contracts, causal debugging and one uncertain behavior at a time; no universal TDD |
64
+ | [Cursor / pstack](https://github.com/cursor/plugins/tree/e43c7ee26e0038c6c1fa8380dd34ce86ff94cb2a/pstack) | Interface rationale, focused bug replay and defensible benchmarks; no playbook-stage machinery |
65
+ | [Kit Langton's Effect skill](https://github.com/kitlangton/skills/tree/71195e36024ebd2e66c5beac946e81eafd5abc43/skills/effect) | Schema construction/optionality, checked against actual [Effect 4 source](https://github.com/Effect-TS/effect/tree/67ba4e46a11ccda0b6761578bfd22c04ae00167d/packages/effect/src) |
66
+ | [Vercel's agent skills](https://github.com/vercel-labs/agent-skills/tree/063bee94c3f4df8453406c830b0a7df0f2860278/skills) | Independent I/O and narrow subscriptions, translated to Effect/atoms; no manual React memoization |
67
+ | [Anthropic frontend-design](https://github.com/anthropics/skills/tree/main/skills/frontend-design), [Taste Skill](https://github.com/Leonxlnx/taste-skill), [Vercel design guidelines](https://vercel.com/design/guidelines) | Task-led composition, typography and useful copy; no forced novelty, arbitrary aesthetic bans or plugin bundle |
68
+ | [Emil Kowalski's skills](https://github.com/emilkowalski/skills/tree/e8a175de22ae1e49370fc144c1f3bb9aeedf988d/skills) | Realistic layout stress, interruptible motion and mobile checks; no fixed variant picker or audit fan-out. Performance caveats checked against [Motion docs](https://motion.dev/docs/performance) |
69
+ | [Dillon Mulroy's skills](https://github.com/dmmulroy/skills/tree/8603380821fee6a77c82639f364ce8fe4f5a92be) and [anti-slop](https://github.com/dmmulroy/anti-slop/tree/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b) | Reviewed Effect/error and cleanup approaches; retain our own service layout and ownership |
70
+ | [Awesome DESIGN.md](https://github.com/VoltAgent/awesome-design-md) and [Impeccable](https://github.com/pbakaus/impeccable) | Reviewed reference collections; community brand analyses are inspiration, not authoritative/current tokens |
71
+
72
+ These are selectively adapted ideas, not imported frameworks. Concrete behavior, existing stack and settled user decisions govern the maintained skills.
package/dist/install.js CHANGED
@@ -24685,7 +24685,7 @@ const runWith = (command, config) => {
24685
24685
  };
24686
24686
  //#endregion
24687
24687
  //#region package.json
24688
- var version = "0.1.761";
24688
+ var version = "0.1.785";
24689
24689
  //#endregion
24690
24690
  //#region src/install.ts
24691
24691
  const cli = pipe(make("deslop-coding-standards", {}, fnUntraced(function* () {
package/dist/oxlint.d.ts CHANGED
@@ -4093,7 +4093,6 @@ export declare const oxlint: {
4093
4093
  '@deslop/coding-standards/no-double-nullish-check': "error";
4094
4094
  '@deslop/coding-standards/no-effect-property-arrow': "error";
4095
4095
  '@deslop/coding-standards/no-error-message-assertion': "error";
4096
- '@deslop/coding-standards/no-fail-in-generator': "error";
4097
4096
  '@deslop/coding-standards/no-fake-ref-state': "error";
4098
4097
  '@deslop/coding-standards/no-hand-written-guard': "error";
4099
4098
  '@deslop/coding-standards/no-let': "error";
@@ -4320,6 +4319,7 @@ export declare const oxlint: {
4320
4319
  'max-depth': ["error", number];
4321
4320
  'max-nested-callbacks': ["error", number];
4322
4321
  'no-cond-assign': "error";
4322
+ 'no-constant-binary-expression': "error";
4323
4323
  'no-continue': "error";
4324
4324
  'no-control-regex': "error";
4325
4325
  'no-debugger': "error";
@@ -4335,6 +4335,7 @@ export declare const oxlint: {
4335
4335
  'no-invalid-regexp': "error";
4336
4336
  'no-iterator': "error";
4337
4337
  'no-lonely-if': "error";
4338
+ 'no-loss-of-precision': "error";
4338
4339
  'no-misleading-character-class': "error";
4339
4340
  'no-multi-assign': "error";
4340
4341
  'no-negated-condition': "error";
@@ -4504,6 +4505,9 @@ export declare const oxlint: {
4504
4505
  'no-unneeded-ternary': "error";
4505
4506
  'no-unreachable-loop': "error";
4506
4507
  'no-unsafe-finally': "error";
4508
+ 'no-unsafe-optional-chaining': ["error", {
4509
+ disallowArithmeticOperators: true;
4510
+ }];
4507
4511
  'no-unused-expressions': "error";
4508
4512
  'no-useless-assignment': "error";
4509
4513
  'no-useless-backreference': "error";
@@ -4515,6 +4519,7 @@ export declare const oxlint: {
4515
4519
  'no-useless-escape': "error";
4516
4520
  'no-useless-rename': "error";
4517
4521
  'no-useless-return': "error";
4522
+ 'no-var': "error";
4518
4523
  'no-void': "error";
4519
4524
  'object-shorthand': "error";
4520
4525
  'oxc/branches-sharing-code': "error";
@@ -4528,6 +4533,7 @@ export declare const oxlint: {
4528
4533
  'prefer-template': "error";
4529
4534
  'require-unicode-regexp': "error";
4530
4535
  'react-doctor/no-call-component-as-function': "error";
4536
+ 'react-doctor/no-controlled-input-value-without-state-update': "error";
4531
4537
  'react-doctor/no-create-context-in-render': "error";
4532
4538
  'react-doctor/no-default-props': "error";
4533
4539
  'react-doctor/no-inline-exhaustive-style': "error";
package/dist/oxlint.js CHANGED
@@ -612,7 +612,7 @@ Object.freeze({
612
612
  }
613
613
  });
614
614
  //#endregion
615
- //#region ../../node_modules/.pnpm/oxlint@1.85.0_oxlint-tsgolint@7.0.2003_vite-plus@1.0.0_@opentelemetry+api@1.9.1_@types+_4709a04ba252a52ff927b8b254462122/node_modules/oxlint/dist/index.js
615
+ //#region ../../node_modules/.pnpm/oxlint@1.85.0_oxlint-tsgolint@7.0.2003_vite-plus@1.0.0_@opentelemetry+api@1.9.1_@types+_bd1fc584f29238a6938d999624a6e509/node_modules/oxlint/dist/index.js
616
616
  /**
617
617
  * Define an Oxlint configuration with type inference.
618
618
  *
@@ -668,14 +668,6 @@ function importedMember(input) {
668
668
  source: "effect"
669
669
  }) && (input.propertyName === void 0 || contains(memberName(input.node), input.propertyName));
670
670
  }
671
- function effectBuilderName(input) {
672
- const builder = input.node.type === "CallExpression" ? input.node.callee : input.node;
673
- return builder.type === "MemberExpression" && importedMember({
674
- context: input.context,
675
- importedName: "Effect",
676
- node: builder
677
- }) ? memberName(builder) : none();
678
- }
679
671
  function isSchemaOperationName(name) {
680
672
  return /^(?:(?:decode|encode)(?:Unknown)?(?:Effect|Exit|Option|Promise|Result|Sync)|asserts|is)$/u.test(name);
681
673
  }
@@ -892,33 +884,10 @@ const noArrayWrapTernary = defineRule({
892
884
  });
893
885
  //#endregion
894
886
  //#region src/rules/no-constant-function.ts
895
- function returnsConstant(node) {
896
- const returned = returnedExpression(node);
897
- return isNotNullish(returned) && (returned.type === "Literal" || returned.type === "Identifier" || returned.type === "MemberExpression" || returned.type === "TemplateLiteral" && isArrayEmpty(returned.expressions));
898
- }
899
- function functionScope(input) {
900
- if (input.scope.block === input.node) return some(input.scope);
901
- return findFirst(input.scope.childScopes, (child) => child.block === input.node);
902
- }
903
- function scopeReferences(scope) {
904
- return appendAll(scope.references, flatMap$1(scope.childScopes, scopeReferences));
905
- }
906
- function isImportedVariable(variable) {
907
- return isArrayNonEmpty(variable.defs) && every(variable.defs, (definition) => definition.type === "ImportBinding");
908
- }
909
- function resolvesToImport(reference) {
910
- return pipe(variableFromScope({
911
- name: reference.identifier.name,
912
- scope: reference.from
913
- }), exists(isImportedVariable));
914
- }
915
887
  function isConstantFunction(input) {
916
- if (input.node.async || input.node.generator || isNotNullish(input.node.typeParameters)) return false;
917
- if (some$1(input.spans, (span) => span.start >= input.node.start && span.end <= input.node.end)) return false;
918
- return returnsConstant(input.node) && pipe(functionScope({
919
- node: input.node,
920
- scope: input.context.sourceCode.getScope(input.node)
921
- }), exists((scope) => every(scopeReferences(scope), resolvesToImport))) && pipe(bindingName(input.node), flatMap((name) => variableFromScope({
888
+ if (input.node.async || input.node.generator || isArrayNonEmpty(input.node.params) || isNotNullish(input.node.typeParameters)) return false;
889
+ const returned = returnedExpression(input.node);
890
+ return isNotNullish(returned) && (returned.type === "Literal" && !hasProperty(returned, "regex") || returned.type === "TemplateLiteral" && isArrayEmpty(returned.expressions)) && pipe(bindingName(input.node), flatMap((name) => variableFromScope({
922
891
  name,
923
892
  scope: input.context.sourceCode.getScope(input.node)
924
893
  })), exists((variable) => every(filter$1(variable.references, (reference) => reference.isRead()), (reference) => reference.identifier.parent.type === "CallExpression" && reference.identifier.parent.callee === reference.identifier)));
@@ -928,20 +897,15 @@ function isConstBound(node) {
928
897
  }
929
898
  const noConstantFunction = defineRule({
930
899
  create: (context) => {
931
- const spans = make$2(empty());
932
- function mark(node) {
933
- update(spans, append(node));
934
- }
935
900
  function report(node) {
936
901
  if (isConstantFunction({
937
902
  context,
938
- node,
939
- spans: get$3(spans)
903
+ node
940
904
  }) && !singleUseThunk({
941
905
  context,
942
906
  node
943
907
  })) context.report({
944
- message: "This function reads nothing but imports, so it is a value: hold its result in a const.",
908
+ message: "Inline this argument-free literal return at its call sites.",
945
909
  node
946
910
  });
947
911
  }
@@ -952,31 +916,33 @@ const noConstantFunction = defineRule({
952
916
  "FunctionDeclaration:exit": report,
953
917
  "FunctionExpression:exit": (node) => {
954
918
  if (isConstBound(node)) report(node);
955
- },
956
- JSXElement: mark,
957
- JSXFragment: mark,
958
- ThisExpression: mark
919
+ }
959
920
  };
960
921
  },
961
922
  meta: { type: "problem" }
962
923
  });
963
924
  //#endregion
964
925
  //#region src/rules/no-deep-pipe.ts
965
- function isPipeCall(node) {
966
- return node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "pipe";
926
+ function isPipeCall(context, node) {
927
+ return node.type === "CallExpression" && node.callee.type === "Identifier" && isImportBinding({
928
+ context,
929
+ importedName: "pipe",
930
+ node: node.callee,
931
+ source: "effect"
932
+ });
967
933
  }
968
- function enclosingPipes(node) {
934
+ function enclosingPipes(context, node) {
969
935
  if (node.type === "Program" || contains$1([
970
936
  "ArrowFunctionExpression",
971
937
  "FunctionDeclaration",
972
938
  "FunctionExpression"
973
939
  ], node.type)) return 0;
974
- const nested = isPipeCall(node.parent) && some$1(node.parent.arguments, (argument) => argument === node);
975
- return enclosingPipes(node.parent) + (nested ? 1 : 0);
940
+ const nested = isPipeCall(context, node.parent) && some$1(node.parent.arguments, (argument) => argument === node);
941
+ return enclosingPipes(context, node.parent) + (nested ? 1 : 0);
976
942
  }
977
943
  const noDeepPipe = defineRule({
978
944
  create: (context) => ({ CallExpression: (node) => {
979
- if (isPipeCall(node) && enclosingPipes(node) >= 3) context.report({
945
+ if (isPipeCall(context, node) && enclosingPipes(context, node) >= 3) context.report({
980
946
  message: "Flatten this pipeline; pipe calls nest at most three deep.",
981
947
  node
982
948
  });
@@ -985,23 +951,61 @@ const noDeepPipe = defineRule({
985
951
  });
986
952
  //#endregion
987
953
  //#region src/rules/no-destructured-parameter.ts
988
- function isPattern(node) {
989
- const target = node.type === "AssignmentPattern" ? node.left : node;
990
- if (target.type === "ArrayPattern") return true;
954
+ function namedTuple(input) {
955
+ if (contains$1(input.seen, input.node)) return false;
956
+ if (input.node.type === "TSTupleType") return isArrayNonEmpty(input.node.elementTypes) && every(input.node.elementTypes, (element) => element.type === "TSNamedTupleMember");
957
+ if (input.node.type === "TSParenthesizedType" || input.node.type === "TSTypeOperator" && input.node.operator === "readonly") return namedTuple({
958
+ ...input,
959
+ node: input.node.typeAnnotation,
960
+ seen: append(input.seen, input.node)
961
+ });
962
+ if (input.node.type !== "TSTypeReference" || input.node.typeName.type !== "Identifier") return false;
963
+ return pipe(variableFromScope({
964
+ name: input.node.typeName.name,
965
+ scope: input.context.sourceCode.getScope(input.node)
966
+ }), exists((variable) => some$1(variable.defs, (definition) => definition.node.type === "TSTypeAliasDeclaration" && namedTuple({
967
+ context: input.context,
968
+ node: definition.node.typeAnnotation,
969
+ seen: append(input.seen, input.node)
970
+ }))));
971
+ }
972
+ function component(node) {
973
+ const declared = node.type === "ArrowFunctionExpression" ? void 0 : node.id;
974
+ const assigned = node.parent.type === "VariableDeclarator" && node.parent.id.type === "Identifier" ? node.parent.id : void 0;
975
+ const name = declared ?? assigned;
976
+ if (name === void 0 || !/^[A-Z]/u.test(name.name) || node.params.length !== 1 || node.body === null) return false;
977
+ if (node.body.type === "JSXElement" || node.body.type === "JSXFragment") return true;
978
+ return node.body.type === "BlockStatement" && some$1(node.body.body, (statement) => statement.type === "ReturnStatement" && (statement.argument?.type === "JSXElement" || statement.argument?.type === "JSXFragment"));
979
+ }
980
+ function isPattern(input) {
981
+ const target = input.node.type === "AssignmentPattern" ? input.node.left : input.node;
982
+ if (target.type === "ArrayPattern") return !exists(fromNullishOr(target.typeAnnotation), (annotation) => namedTuple({
983
+ context: input.context,
984
+ node: annotation.typeAnnotation,
985
+ seen: []
986
+ }));
991
987
  if (target.type !== "ObjectPattern") return false;
992
988
  const named = filter$1(target.properties, (property) => property.type === "Property");
993
- return !(named.length === 1 && named[0]?.key.type === "Identifier" && named[0].key.name === "ref" && some$1(target.properties, (property) => property.type === "RestElement"));
989
+ return !(input.component && named.length === 1 && named[0]?.key.type === "Identifier" && named[0].key.name === "ref" && some$1(target.properties, (property) => property.type === "RestElement"));
994
990
  }
995
991
  const noDestructuredParameter = defineRule({
996
992
  create: (context) => {
997
993
  function reportParameters(node) {
998
- for (const parameter of filter$1(node.params, isPattern)) context.report({
994
+ for (const parameter of filter$1(node.params, (candidate) => isPattern({
995
+ component: component(node),
996
+ context,
997
+ node: candidate
998
+ }))) context.report({
999
999
  message: "Take the value whole and read its fields instead of destructuring it.",
1000
1000
  node: parameter
1001
1001
  });
1002
1002
  }
1003
1003
  function reportLoopVariable(node) {
1004
- if (node.left.type === "VariableDeclaration" && some$1(node.left.declarations, (declarator) => isPattern(declarator.id))) context.report({
1004
+ if (node.left.type === "VariableDeclaration" && some$1(node.left.declarations, (declarator) => isPattern({
1005
+ component: false,
1006
+ context,
1007
+ node: declarator.id
1008
+ }))) context.report({
1005
1009
  message: "Take the value whole and read its fields instead of destructuring it.",
1006
1010
  node: node.left
1007
1011
  });
@@ -1125,50 +1129,26 @@ const noErrorMessageAssertion = defineRule({
1125
1129
  meta: { type: "problem" }
1126
1130
  });
1127
1131
  //#endregion
1128
- //#region src/rules/no-fail-in-generator.ts
1129
- function insideEffectGenerator(input) {
1130
- if (input.node.type === "Program") return false;
1131
- if ((input.node.type === "ArrowFunctionExpression" || input.node.type === "FunctionDeclaration" || input.node.type === "FunctionExpression") && !input.node.generator) return false;
1132
- if (input.node.type === "FunctionExpression" && input.node.generator && input.node.parent.type === "CallExpression" && exists(effectBuilderName({
1133
- context: input.context,
1134
- node: input.node.parent.callee
1135
- }), (name) => contains$1([
1136
- "fn",
1137
- "fnUntraced",
1138
- "gen"
1139
- ], name))) return true;
1140
- return insideEffectGenerator({
1141
- context: input.context,
1142
- node: input.node.parent
1143
- });
1144
- }
1145
- const noFailInGenerator = defineRule({
1146
- create: (context) => ({ CallExpression: (node) => {
1147
- if (node.callee.type === "MemberExpression" && importedMember({
1148
- context,
1149
- importedName: "Effect",
1150
- node: node.callee,
1151
- propertyName: "fail"
1152
- }) && node.parent.type === "YieldExpression" && node.parent.delegate && node.arguments[0]?.type !== "NewExpression" && insideEffectGenerator({
1153
- context,
1154
- node
1155
- })) context.report({
1156
- message: "Yield the error instead: return yield* E.make(...).",
1157
- node
1158
- });
1159
- } }),
1160
- meta: { type: "problem" }
1161
- });
1162
- //#endregion
1163
1132
  //#region src/rules/no-fake-ref-state.ts
1164
1133
  function propertyName(node) {
1165
1134
  if (!node.computed && node.key.type === "Identifier") return some(node.key.name);
1166
1135
  if (node.key.type === "Literal" && isString(node.key.value)) return some(node.key.value);
1167
1136
  return none();
1168
1137
  }
1138
+ function hasUnusedSetter(input) {
1139
+ const declaration = input.node.parent;
1140
+ if (declaration.type !== "VariableDeclarator" || declaration.id.type !== "ArrayPattern") return false;
1141
+ const setter = declaration.id.elements[1];
1142
+ if (isNullish(setter)) return true;
1143
+ if (setter.type !== "Identifier") return false;
1144
+ return pipe(variableFromScope({
1145
+ name: setter.name,
1146
+ scope: input.context.sourceCode.getScope(setter)
1147
+ }), exists((variable) => !some$1(variable.references, (reference) => reference.isRead())));
1148
+ }
1169
1149
  function isFakeRefState(input) {
1170
- if (!isReactUseState(input) || input.node.arguments[0]?.type !== "ArrowFunctionExpression" || input.node.arguments[0].body.type !== "ObjectExpression") return false;
1171
- return some$1(input.node.arguments[0].body.properties, (property) => property.type === "Property" && contains(propertyName(property), "current"));
1150
+ if (!isReactUseState(input) || input.node.arguments[0]?.type !== "ArrowFunctionExpression" || input.node.arguments[0].body.type !== "ObjectExpression" || input.node.arguments[0].body.properties.length !== 1 || !hasUnusedSetter(input)) return false;
1151
+ return some$1(input.node.arguments[0].body.properties, (property) => property.type === "Property" && contains(propertyName(property), "current") && property.value.type === "Literal" && property.value.value === null);
1172
1152
  }
1173
1153
  const noFakeRefState = defineRule({
1174
1154
  create: (context) => ({ CallExpression: (node) => {
@@ -1492,18 +1472,43 @@ function insideCycleType(input) {
1492
1472
  node: input.node.parent
1493
1473
  });
1494
1474
  }
1495
- function referencedName(node) {
1496
- if (node.type !== "TSTypeReference") return none();
1497
- if (node.typeName.type === "Identifier") return some(node.typeName.name);
1498
- return node.typeName.type === "TSQualifiedName" ? some(node.typeName.right.name) : none();
1475
+ function typeDeclarations(input) {
1476
+ if (input.name.type === "Identifier") return pipe(variableFromScope({
1477
+ name: input.name.name,
1478
+ scope: input.context.sourceCode.getScope(input.name)
1479
+ }), flatMap((variable) => variable.scope.block.type === "ClassDeclaration" && some$1(variable.defs, (definition) => definition.node === variable.scope.block) ? variableFromScope({
1480
+ name: variable.name,
1481
+ scope: variable.scope.upper
1482
+ }) : some(variable)), map((variable) => map$1(variable.defs, (definition) => definition.node)), getOrElse(() => empty()));
1483
+ if (input.name.type !== "TSQualifiedName") return [];
1484
+ const name = input.name;
1485
+ return pipe(typeDeclarations({
1486
+ context: input.context,
1487
+ name: name.left
1488
+ }), flatMap$1((declaration) => {
1489
+ if (declaration.type !== "TSModuleDeclaration" || declaration.body === null) return [];
1490
+ return pipe(declaration.body.body, map$1(statementDeclaration), filter$1((node) => node !== null));
1491
+ }), filter$1((declaration) => (declaration.type === "TSTypeAliasDeclaration" || declaration.type === "TSModuleDeclaration") && declaration.id.type === "Identifier" && declaration.id.name === name.right.name));
1499
1492
  }
1500
1493
  function shapeLiterals(input) {
1501
1494
  if (input.type.type === "TSTypeLiteral") return [input.type];
1502
- return pipe(referencedName(input.type), filter((name) => !contains$1(input.seen, name)), map((name) => pipe(input.aliases, filter$1((alias) => alias.id.name === name), flatMap$1((alias) => shapeLiterals({
1503
- aliases: input.aliases,
1504
- seen: append(input.seen, name),
1495
+ if (input.type.type === "TSUnionType" || input.type.type === "TSIntersectionType") return flatMap$1(input.type.types, (type) => shapeLiterals({
1496
+ ...input,
1497
+ type
1498
+ }));
1499
+ if (input.type.type === "TSParenthesizedType") return shapeLiterals({
1500
+ ...input,
1501
+ type: input.type.typeAnnotation
1502
+ });
1503
+ if (input.type.type !== "TSTypeReference") return [];
1504
+ return pipe(typeDeclarations({
1505
+ context: input.context,
1506
+ name: input.type.typeName
1507
+ }), filter$1((declaration) => declaration.type === "TSTypeAliasDeclaration"), filter$1((alias) => !contains$1(input.seen, alias)), flatMap$1((alias) => shapeLiterals({
1508
+ ...input,
1509
+ seen: append(input.seen, alias),
1505
1510
  type: alias.typeAnnotation
1506
- })))), getOrElse(() => empty()));
1511
+ })));
1507
1512
  }
1508
1513
  const noReadonlyTypeSyntax = defineRule({
1509
1514
  create: (context) => {
@@ -1512,7 +1517,6 @@ const noReadonlyTypeSyntax = defineRule({
1512
1517
  program: context.sourceCode.ast
1513
1518
  });
1514
1519
  const shapes = make$2(empty());
1515
- const aliases = make$2(empty());
1516
1520
  const properties = make$2(empty());
1517
1521
  const methods = make$2(empty());
1518
1522
  function exempt(node) {
@@ -1532,7 +1536,7 @@ const noReadonlyTypeSyntax = defineRule({
1532
1536
  },
1533
1537
  "Program:exit": () => {
1534
1538
  const literals = pipe(get$3(shapes), flatMap$1((shape) => shapeLiterals({
1535
- aliases: get$3(aliases),
1539
+ context,
1536
1540
  seen: [],
1537
1541
  type: shape
1538
1542
  })));
@@ -1572,9 +1576,6 @@ const noReadonlyTypeSyntax = defineRule({
1572
1576
  TSPropertySignature: (node) => {
1573
1577
  update(properties, append(node));
1574
1578
  },
1575
- TSTypeAliasDeclaration: (node) => {
1576
- update(aliases, append(node));
1577
- },
1578
1579
  TSTypeOperator: (node) => {
1579
1580
  if (node.operator === "readonly" && !exempt(node)) context.report({
1580
1581
  message: "Use a mutable type shape.",
@@ -1597,7 +1598,7 @@ function isExported(node) {
1597
1598
  if (node.parent.type === "ExportNamedDeclaration" || node.parent.type === "ExportDefaultDeclaration") return true;
1598
1599
  return node.parent.type === "VariableDeclarator" && node.parent.parent.type === "VariableDeclaration" && node.parent.parent.parent.type === "ExportNamedDeclaration";
1599
1600
  }
1600
- function inferredIdentically$1(input) {
1601
+ function inferredIdentically(input) {
1601
1602
  const text = input.context.sourceCode.getText;
1602
1603
  const body = input.node.body;
1603
1604
  if (body === null) return false;
@@ -1620,7 +1621,7 @@ const noRedundantReturnType = defineRule({
1620
1621
  const frame = last(get$3(frames));
1621
1622
  update(frames, dropRight(1));
1622
1623
  if (node.async || node.generator || isNullish(node.returnType)) return;
1623
- if (inferredIdentically$1({
1624
+ if (inferredIdentically({
1624
1625
  context,
1625
1626
  node,
1626
1627
  returns: match(frame, {
@@ -1710,14 +1711,6 @@ const noRedundantUseRefNullType = defineRule({
1710
1711
  });
1711
1712
  //#endregion
1712
1713
  //#region src/rules/no-redundant-variable-annotation.ts
1713
- function isStringLiteral(node) {
1714
- return node.type === "Literal" && isString(node.value);
1715
- }
1716
- function inferredIdentically(input) {
1717
- const text = input.context.sourceCode.getText;
1718
- if (input.init.type === "TSAsExpression") return text(input.init.typeAnnotation) === text(input.type);
1719
- return input.init.type === "ConditionalExpression" && input.type.type === "TSStringKeyword" && isStringLiteral(input.init.consequent) && isStringLiteral(input.init.alternate);
1720
- }
1721
1714
  const noRedundantVariableAnnotation = defineRule({
1722
1715
  create: (context) => ({ VariableDeclarator: (node) => {
1723
1716
  const annotation = node.id.typeAnnotation;
@@ -1729,11 +1722,7 @@ const noRedundantVariableAnnotation = defineRule({
1729
1722
  });
1730
1723
  return;
1731
1724
  }
1732
- if (inferredIdentically({
1733
- context,
1734
- init: node.init,
1735
- type: annotation.typeAnnotation
1736
- })) context.report({
1725
+ if (node.init.type === "TSAsExpression" && context.sourceCode.getText(node.init.typeAnnotation) === context.sourceCode.getText(annotation.typeAnnotation)) context.report({
1737
1726
  message: "Drop this annotation; inference gives the same type.",
1738
1727
  node: annotation
1739
1728
  });
@@ -2153,7 +2142,7 @@ function schemaDefinitionMember(node) {
2153
2142
  return none();
2154
2143
  }
2155
2144
  function isSchemaDefinitionName(name) {
2156
- return !isSchemaOperationName(name) && !/^to[A-Z]/u.test(name);
2145
+ return !isSchemaOperationName(name) && !/^(?:is|to)[A-Z]/u.test(name) && !/^makeFilter(?:Group)?$/u.test(name);
2157
2146
  }
2158
2147
  function isSchemaDefinition(input) {
2159
2148
  if (input.node.type === "CallExpression" && input.node.callee.type === "Identifier" && isImportBinding({
@@ -2361,7 +2350,6 @@ const oxlint = defineConfig({
2361
2350
  "@deslop/coding-standards/no-double-nullish-check": "error",
2362
2351
  "@deslop/coding-standards/no-effect-property-arrow": "error",
2363
2352
  "@deslop/coding-standards/no-error-message-assertion": "error",
2364
- "@deslop/coding-standards/no-fail-in-generator": "error",
2365
2353
  "@deslop/coding-standards/no-fake-ref-state": "error",
2366
2354
  "@deslop/coding-standards/no-hand-written-guard": "error",
2367
2355
  "@deslop/coding-standards/no-let": "error",
@@ -2638,6 +2626,7 @@ const oxlint = defineConfig({
2638
2626
  "max-depth": ["error", 3],
2639
2627
  "max-nested-callbacks": ["error", 6],
2640
2628
  "no-cond-assign": "error",
2629
+ "no-constant-binary-expression": "error",
2641
2630
  "no-continue": "error",
2642
2631
  "no-control-regex": "error",
2643
2632
  "no-debugger": "error",
@@ -2651,6 +2640,7 @@ const oxlint = defineConfig({
2651
2640
  "no-invalid-regexp": "error",
2652
2641
  "no-iterator": "error",
2653
2642
  "no-lonely-if": "error",
2643
+ "no-loss-of-precision": "error",
2654
2644
  "no-misleading-character-class": "error",
2655
2645
  "no-multi-assign": "error",
2656
2646
  "no-negated-condition": "error",
@@ -2888,6 +2878,7 @@ const oxlint = defineConfig({
2888
2878
  "no-unneeded-ternary": "error",
2889
2879
  "no-unreachable-loop": "error",
2890
2880
  "no-unsafe-finally": "error",
2881
+ "no-unsafe-optional-chaining": ["error", { disallowArithmeticOperators: true }],
2891
2882
  "no-unused-expressions": "error",
2892
2883
  "no-useless-assignment": "error",
2893
2884
  "no-useless-backreference": "error",
@@ -2899,6 +2890,7 @@ const oxlint = defineConfig({
2899
2890
  "no-useless-escape": "error",
2900
2891
  "no-useless-rename": "error",
2901
2892
  "no-useless-return": "error",
2893
+ "no-var": "error",
2902
2894
  "no-void": "error",
2903
2895
  "object-shorthand": "error",
2904
2896
  "oxc/branches-sharing-code": "error",
@@ -2910,6 +2902,7 @@ const oxlint = defineConfig({
2910
2902
  "prefer-template": "error",
2911
2903
  "require-unicode-regexp": "error",
2912
2904
  "react-doctor/no-call-component-as-function": "error",
2905
+ "react-doctor/no-controlled-input-value-without-state-update": "error",
2913
2906
  "react-doctor/no-create-context-in-render": "error",
2914
2907
  "react-doctor/no-default-props": "error",
2915
2908
  "react-doctor/no-inline-exhaustive-style": "error",
@@ -3048,7 +3041,6 @@ var oxlint_default = definePlugin({
3048
3041
  "no-double-nullish-check": noDoubleNullishCheck,
3049
3042
  "no-effect-property-arrow": noEffectPropertyArrow,
3050
3043
  "no-error-message-assertion": noErrorMessageAssertion,
3051
- "no-fail-in-generator": noFailInGenerator,
3052
3044
  "no-fake-ref-state": noFakeRefState,
3053
3045
  "no-hand-written-guard": noHandWrittenGuard,
3054
3046
  "no-let": noLet,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deslop/coding-standards",
3
- "version": "0.1.761",
3
+ "version": "0.1.785",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/MP281X/deslop",
@@ -5,103 +5,115 @@ description: 'UI and UX rules for screens and visual explanations. Use when buil
5
5
 
6
6
  # Design
7
7
 
8
- **Scope.** Follow repository-specific CODING_STANDARDS.md. CLI-installed skills are read-only; improve their package source and refresh via the CLI.
8
+ **Scope.** Follow repository-specific CODING_STANDARDS.md and engineering.
9
9
 
10
- **Catalog.** List installed components and current shadcn registry capabilities before designing; group the available primitives by purpose. Inspect source/props rather than assuming training-era APIs. Reuse shared components; add missing registry components through the repository command, never hand-build or restyle them. Lint violations use existing tokens/variants.
10
+ ## Decide
11
11
 
12
- **Code.** Reuse one app host, compiler, theme, shared components and Tailwind tokens. No custom-CSS/dynamic-class escape or weaker scratch checks. Verify the first real slice before expanding.
12
+ **Direction.** Start from the actual task, audience, content and settled preferences. Make the next user action and its relevant content dominant, not generic "premium" styling. When a reference is useful, inspect the relevant accepted one for transferable hierarchy, density and interaction choices; distinguish observed details from inferred ones. Community brand analyses are inspiration, not verified current tokens. Keep durable visual choices at their owner, not repeated briefs. Ask only about a consequential tradeoff the evidence cannot settle; another agent cannot establish user taste.
13
13
 
14
- ```tsx
15
- // good
16
- <Badge variant="secondary">Draft</Badge>
17
- <Empty><EmptyTitle>No runs yet</EmptyTitle><Button onClick={() => startRun()}>Run</Button></Empty>
18
- // bad — "without hardcoding those values the desing/ui/ux need to be consistent with the rest of the app"
19
- <span className="rounded-full bg-[#eef] px-[7px] text-[11px]">Draft</span>
20
- <div className="flex flex-col items-center gap-2 p-6 text-muted-foreground">No runs yet</div>
21
- ```
14
+ **One meaning.** Each visible element must add information, enable a distinct action or clarify a relationship. Delete repeated headings, summaries, counts, status badges, decorative wrappers and controls that say or do the same thing. Remove explanations that merely restate the visible example/state. Keyboard shortcuts, screen-reader labels and responsive placement are not redundant visible controls; preserve their function.
15
+
16
+ **Scan-first.** Put the task, relevant content and next action where they can be identified without reading a paragraph. Use stable visual roles, not another card/badge/icon for every fact. Expose needed detail on demand, never hide a consequential warning or essential action.
22
17
 
23
- **Consistency.** Match nearest screens: structure, spacing, type, icons, density, states, copy and behavior. Reuse their renderer; follow the majority where inconsistent. Without a sibling, use T3 Code's interface as reference.
18
+ | Content | Preferred treatment |
19
+ | -------------------------- | ------------------------------------------------------------------------- |
20
+ | Comparable records/options | Aligned rows/columns; consistent order, units and metadata placement |
21
+ | Familiar action | Conventional icon-only control with an accessible name; one visible owner |
22
+ | Ambiguous action | Clear text label, not an invented/guessed icon or redundant icon + text |
23
+ | Explanation | Real state/example first; text only for missing context or consequences |
24
24
 
25
25
  ```tsx
26
- // good — the picker every other model choice uses
27
- <ModelPicker value={model} onValueChange={setModel} />
28
- // bad — "make how the button behave consistent with the rest to not break the user expectations"
29
- <Select value={model} onValueChange={setModel}>{models.map(...)}</Select> // a second picker that behaves differently
26
+ // one visible action, named for assistive technology
27
+ <Button aria-label="Rename" size="icon"><PencilIcon aria-hidden /></Button>
28
+ // redundant label for an already-obvious action
29
+ <Button><PencilIcon />Rename</Button>
30
30
  ```
31
31
 
32
- **One way.** One representation/action, one consistent name and appearance. Remove redundant refresh, badges, icon-label pairs, decoration and wrappers.
32
+ Do not remove necessary names, status, comfortable hit targets or instructions merely to make a screen smaller.
33
33
 
34
- ```tsx
35
- // good
36
- <StarIcon aria-label="Default" />
37
- // bad — "i don't want refresh buttons or stuffs like that in the ui/app"
38
- <Button onClick={refetch}><RefreshIcon /></Button>
39
- <Badge><StarIcon /> Default</Badge> // "Don't have icons/emoji and text that say the same thing"
40
- ```
34
+ ## Build
41
35
 
42
- **Compact.** Flat and dense: group by proximity before adding a card, cards only when elevation means something and never nested, and no empty vertical space.
36
+ **Canonical components.** Reuse the existing renderer, states and interactions for the same role. Inspect its relevant source/API, not a full catalogue; add missing primitives through the repository registry command. Fix wrong shared patterns at their owner when delivering retained code, not before an exploratory preview. Without a sibling, use the supplied reference and component defaults within settled preferences.
43
37
 
44
38
  ```tsx
45
- // good
46
- <section className="flex flex-col gap-2">{rows}</section>
47
- // bad — "keep the UI flat/brutalist/minimal… don't have nested cards or visually heavy layouts"
48
- <Card><CardContent><Card className="p-8">{rows}</Card></CardContent></Card>
39
+ // same model-selection control used elsewhere
40
+ <ModelPicker value={model} onValueChange={setModel} />
41
+ // another representation of the same job
42
+ <Select value={model} onValueChange={setModel} />
49
43
  ```
50
44
 
51
- **Precision.** Exact on the design-system scale: icons sized by one shared box, one horizontal inset for search, pinned rows, dividers, and list rows; pinned rows outside the scroll area; borders that stay put while scrolling.
45
+ **Tokens and structure.** Use the existing app host, compiler, theme, shared components and Tailwind tokens. No custom-CSS/dynamic-class escape, hardcoded color/size exception or weakened shared rules. Keep surfaces flat and dense; group by proximity before adding borders or elevation. No nested cards or wrappers that merely label content again. Whitespace must support hierarchy, separation or comfortable use, not fill the page.
52
46
 
53
47
  ```tsx
54
- // good
55
- <SearchIcon className="size-4" />
56
- // bad — "some icons have different sizes", "the spacing is kinda shitty"
57
- <SearchIcon width={15} height={15} />
48
+ // existing primitives and semantic tokens
49
+ <Empty><EmptyTitle>No runs yet</EmptyTitle><Button onClick={startRun}>Run</Button></Empty>
50
+ // parallel hand-built appearance
51
+ <div className="rounded-full bg-[#eef] px-[7px] text-[11px]">No runs yet</div>
58
52
  ```
59
53
 
60
- **Visible.** Controls are always visible, outlined or filled, never only on hover; state shows by background (the selected row, the active tab), never a check column; when rows share a name, the subtitle shows what tells them apart.
54
+ **Typography.** Give headings, navigation, body text and data deliberate roles through the existing type scale, weights, line heights and readable line lengths. Use tabular numerals where values need comparison; let prose wrap without making controls inconsistent. Preserve established fonts and tokens rather than changing them for novelty. Do not substitute tiny labels, gratuitous capitals or isolated accent words for hierarchy.
61
55
 
62
- ```tsx
63
- // good
64
- <TableRow data-state={selected ? 'selected' : undefined}>
65
- // bad — "i'm not a fan that things like the empty star comes out on hover"
66
- <Button className="opacity-0 group-hover:opacity-100" variant="ghost">
67
- ```
56
+ **Copy.** Name actions from the user's task, not implementation internals. Keep the same vocabulary across controls and confirmations. Use plain verbs and sentence case; labels describe outcomes, not decoration. Empty states offer a relevant next action; errors identify the failed action and available recovery. Reuse its existing control rather than repeating the action in text or adding another button; do not invent a recovery that cannot work.
68
57
 
69
- **Responsive.** Preserve the complete task on phones, not merely stacked desktop panes. Test narrow/wide screens, long/duplicate content, zoom, overflow, touch targets and reachable controls; no hidden essential action or deep nesting.
58
+ | Weak | Useful |
59
+ | ---------------------- | ----------------------------------------------------------------- |
60
+ | `Submit` | `Save changes` → `Changes saved` |
61
+ | `Something went wrong` | `Couldn't save.` beside the existing retry action; keep the draft |
62
+ | `No data` | `No notes yet` beside the existing create action |
70
63
 
71
- **Accessible.** Semantic landmarks, headings, labels and status/errors; keyboard navigation, visible focus, logical order and deliberate return, contrast, reduced motion and accessible names. Run axe and real keyboard/touch journeys; automated scans alone do not prove accessibility.
64
+ **Alignment.** Give related headings, search, pinned rows, dividers and records one owning grid/inset. Headers and row contents share columns; action columns stay fixed when labels wrap or are absent. Use the same component/size variant for equivalent controls: icon box/stroke, hit target, control height, baseline and spacing. Pinned content stays outside the scroll area; borders do not jump while scrolling. Fix the owning layout/component, not per-instance offsets; matching boxes alone does not establish optical alignment or legibility. Trace the cause before applying these repairs:
72
65
 
73
- **Feedback.** Quiet local mutation feedback; pending/error state beside its action or field. Motion explains change, never delays input/navigation/reading. Use existing tokens and respect reduced motion; no animation dependency for a CSS state change.
66
+ | Defect | Check | Owning repair |
67
+ | ---------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- |
68
+ | Squashed icon/avatar | Fixed-size flex child shrinks | Preserve its canonical box with `shrink-0` |
69
+ | Trailing action disappears | Content track refuses to shrink | `min-w-0` on content; `shrink-0` on the control, or a shrinkable grid track |
70
+ | Wrapped rows look adrift | Center alignment against variable-height text | Consistent, deliberate top/baseline alignment |
71
+ | Identifiers look identical | Truncation hides the distinguishing segment | Preserve that segment; expose the full value through an existing keyboard/touch-accessible surface |
72
+ | Counts shift during updates | Proportional digits/inconsistent formatting | `tabular-nums` + consistent locale/units |
73
+ | Accents/tall scripts clip | Tight line height or text-box clipping | Fix the owning type-scale/overflow choice; don't shrink the font to fit |
74
+ | Missing metadata leaves separators | Empty optional slots still render | Omit orphaned separators/lines; reserve space only when the layout needs it |
74
75
 
75
- **Inspect.** Open actual desktop/phone captures: hierarchy, density, legibility, alignment, clipping, controls. Inspect meaningful video frames and sequence. Fix weak composition even when clicks/axe pass; a playable recording is not inspection.
76
+ **Interaction and state.** Controls remain discoverable, never hover-only. Use the canonical selected-row/active-tab treatment; do not add a second badge or check column that echoes it. Keep pending/error feedback beside its owner and preserve recoverable input. No redundant refresh, copied state or polling shortcut around the existing reactive integration.
76
77
 
77
- ## Prototypes
78
+ **Motion.** Explain change without delaying input or moving content just for decoration. Reuse existing tokens/components; no new library for a fade or universal curve, duration, bounce or press scale.
78
79
 
79
- **Task.** Name the person, next action and uncertainty. Compare the same realistic task/data; hierarchy follows editing, scanning, comparison, graph exploration or review.
80
+ | Interaction | Treatment |
81
+ | -------------------------------------- | -------------------------------------------------------------------------------------------------- |
82
+ | Typing, shortcuts, frequent navigation | Immediate response; no decorative movement |
83
+ | Trigger-anchored surface | Enter/exit from its trigger; a centered unanchored modal keeps a centered origin |
84
+ | Rapid toggle/reversal | Retarget from the visible state; no input lock, stale-target snap or wait for the prior transition |
85
+ | Drag/gesture | Follow the pointer; preserve the existing primitive's capture, cancellation and release behavior |
86
+ | Reduced motion | Preserve feedback through a suitable static or gentle non-spatial equivalent |
80
87
 
81
- **Range.** Show at least five clearly different directions across information model, interaction, layout, density and visual language—not cosmetic variations. Include an ambitious/unexpected concept and a plain smaller direction; adapt useful real-product patterns. Label executable prototypes versus conceptual sketches and what each tests. Concepts may contribute only an interaction, hierarchy or visual idea; never imply they are working software.
88
+ Use responsive entry easing and brief existing durations. Simple retargetable states suit transitions; gestures may need the existing spring. Prefer transform/opacity where appropriate, but CSS/WAAPI does not guarantee compositor execution. Profile observed performance problems, not every animation. Inspect rapid repetition/reversal and reduced motion, not only the final frame.
82
89
 
83
- **Behavior.** Executable candidates exercise the smallest complete journey plus choice-changing empty, long/duplicate, selection, pending, failure and permission states. Conceptual sketches state unimplemented behavior; judge their idea, not a fictional runtime pass.
90
+ **Access and responsiveness.** Preserve the complete task on phones, not merely stacked desktop panes. Keep essential actions reachable with keyboard, touch and zoom. Use semantic structure, accessible names, visible focus, logical order and deliberate focus return. Inspect long/duplicate content, overflow, contrast and relevant failure/permission states. Do not remove an accessible affordance to satisfy visual minimalism.
84
91
 
85
- **Synthesis.** The user often chooses between directions. Compare transferable strengths/tradeoffs and propose a coherent blend; don't force a winner or concatenate incompatible mechanisms. Test the blend's weakest assumption before expansion.
92
+ | Mobile defect | Check |
93
+ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
94
+ | Hover remains after tap | Capability-aware styles; inspect the framework's handling before adding another gate |
95
+ | Input zoom/wrong keyboard | Readable input sizing + correct input type/mode; never disable browser zoom |
96
+ | Bottom controls disappear | Real browser chrome, software keyboard and safe areas; appropriate supported viewport sizing/insets |
97
+ | Drag blocks scrolling/content can't be copied | Scope gesture/selection restrictions to their owner; preserve scrolling, zoom and selectable content |
86
98
 
87
- **Show.** Labeled, inspected desktop/phone views at consistent sizes; short video only for meaningful interaction. No manual variant-switcher journey. Reuse one host, discard losing source and expand the synthesis. Settled UI verification does not require five more directions.
99
+ Touch and mouse can coexist; do not infer capabilities from user-agent strings or viewport width. No blanket suppression of overscroll, selection or native feedback without a demonstrated need and working replacement. Emulation verifies selected layout cases, not every phone behavior: affected device-specific keyboard/viewport/gesture contracts need target-device proof before completion.
88
100
 
89
- ## Visual explanations
101
+ ## Prototype
90
102
 
91
- **Form.** Markdown for conclusions, comparisons and file maps; visuals for relationships/layout; interaction for exploration; video for change over time. Use the simplest useful form, not images of prose.
103
+ **Try the question.** Build one rough representative screen/interaction in the existing host with realistic data. Inspect enough to make the trial trustworthy, then let the user try it before merge-readiness work. Label stubs and limits; a conceptual sketch is sufficient only when execution is unnecessary, never proof of working software. Set a short first-preview budget; show a partial result/blocker rather than expanding unseen. No production completeness or polished gallery first.
92
104
 
93
- **Hierarchy.** Outcome/decision, decisive evidence, limits. Group by reader question, not execution order; consistent labels and annotations beside their subject. Simplify/split for phones, never shrink unreadable canvases.
105
+ **Use the findings.** For layout questions, compare hierarchy or interaction, not color-only variants. Add attempts only for unresolved uncertainty, feedback or an explicit request—no fixed count; a style question can compare style. Keep useful comparisons; discard/rebuild freely and combine compatible strengths, not incompatible mechanisms. Test the weakest assumption while it can change the choice. A settled direction needs no manufactured taste question or exploration round.
94
106
 
95
- **Review.** Map every semantic/config change to final packages/files; distinguish moves/generated copies and omit formatting noise. Use readable lists/tables, not compressed tree art. Explain behavior and consequential choices without requiring diff reading. Risks stay visible; supporting detail collapses. No host-status duplication or screenshot file browser.
107
+ ## Verify and show
96
108
 
97
- **Delivery.** Markdown carries the explanation. HTML complements it only where spatial relationships, layout or interaction communicate something Markdown cannot; never screenshot or record a replacement for readable prose, tables or file maps. Embed only the useful visual, beside its claim; keep secondary captures collapsed or linked rather than making a gallery the default reading path. Prove actual controls, keyboard access, desktop/phone layout and errors. Keep temporary hosts, source and media under the repository's ignored scratch path, never a root proof directory. Stop owned services and remove settled source. No duplicate chat, page and PR recap.
109
+ **Inspect retained UI.** Open actual captures at the sizes needed for the task. Compare related edges, columns, baselines and equivalent controls across rows/screens—not just successful clicks. Use realistic long names/URLs, translated labels, Unicode, missing fields and relevant 0/1/many records at the actual container width. Respect schema/API limits; never add a validation limit to make layout pass. Mix cases across rows. Check selected/focus/error states and scrolling; a clean default row can hide misalignment. Inspect hierarchy, density, legibility and clipping. Exercise the relevant keyboard/touch, narrow/wide, zoom and focus-return journeys; run axe where applicable. A clean scan does not prove contrast or usability. Inspect meaningful video frames and sequence; a playable file is not proof of a good composition. Batch relevant inspection/repairs; recheck affected views rather than restart a whole audit after each micro-edit. Visible misalignment, inconsistent controls or redundant elements remain defects even when types/tests pass; fix them before claiming acceptance.
98
110
 
99
- **Theme and scale.** Agent-made visuals use the user's dark theme and existing design tokens. Verify browser color preference; a dark class alone does not activate a media-query theme. Align headings, controls and rows to shared insets; use consistent type/spacing and compact grouping, not empty space or tiny text. Show variants separately legible, not as a compressed strip. Inspect hierarchy, density and alignment at desktop/phone sizes; reject a weak composition even when overflow and clicks pass.
111
+ **Show the design.** Use inspected screenshots for layout and short video for behavior. Compare at consistent sizes with states needed for the decision; no variant switcher just to see the options. Prefer the existing app-owned preview/element feedback. A visual must add spatial or interactive information, not reproduce prose, a file list or a fake runtime screenshot. Link useful existing evidence rather than render another explanation page. Use the user's theme and verify the actual browser color preference. Keep temporary source/services in ignored scratch, preserve useful media and retire settled attempts; persistent previews are opt-in.
100
112
 
101
- ## Image assets
113
+ **Review surfaces.** Open on the outcome the user needs to judge, with technical detail available on demand. Group by changed behavior, not files or agent stages. A selected example, diagram or diff must reveal its scope; never make curated evidence look like complete coverage. Do not build a custom review dashboard when existing captures and the PR can answer the question. A design-skill edit is not a product-screen change.
102
114
 
103
- Use image generation for an asset that benefits from synthesis—illustration, atmosphere, texture, an icon exploration—not for ordinary text, an exact diagram or a fake screenshot of functioning software. Use the native generation/editing tool exposed by the assigned harness (Codex image work uses its native image tool); never invent a tool call or substitute an API client when the capability is absent. Missing capability is a reported gap, not a successful generation.
115
+ ## Image assets
104
116
 
105
- Give the tool the asset's purpose, placement, composition, existing palette, intended dimensions/aspect ratio and output constraints. For edits, supply the real source image and name what must remain unchanged. Generate only assets the chosen direction needs; do not add speculative art or an image dependency to an otherwise complete UI. Use repository icons and CSS where they express the job more precisely.
117
+ Use image generation only for a needed illustrative asset, atmosphere, texture or icon exploration—not ordinary text, exact diagrams or fake working UI. Use the native tool exposed by the harness; no invented invocation or replacement API client when unavailable. Report missing capability honestly.
106
118
 
107
- Inspect the actual returned file at its intended display size: composition, subject, crop, text/edge artifacts, contrast and fit with the surrounding screen. Correct visible defects before integrating. Preserve useful original output and identify it as generated; a generated visual is not runtime or browser evidence. Real UI proof still comes from the rendered app and actual interaction. Store only used assets in product source; remove discarded variants and temporary generation material.
119
+ Describe purpose, placement, composition, palette and intended size/aspect ratio. For edits, supply the real source and state what remains unchanged. Generate only what the chosen direction needs; prefer existing icons/CSS when they express the job precisely. Inspect the returned asset at its intended size for crop, text/edge artifacts, contrast and fit. Preserve useful originals and identify generated assets as such; they are not runtime/browser proof. Keep only used assets in product source.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: engineering
3
- description: 'Engineering rules for product code, paired with the shared lint. Use before designing or editing code, writing tests, or reviewing a diff.'
3
+ description: 'TypeScript and Effect code conventions paired with shared lint. Use when designing, implementing or reviewing product/test code, including prototypes.'
4
4
  ---
5
5
 
6
- **Scope.** Apply these rules and repository-specific CODING_STANDARDS.md. CLI-installed copies are read-only; improve their package source, then refresh through the CLI.
6
+ **Scope.** Apply these rules and repository-specific CODING_STANDARDS.md.
7
7
 
8
- **Sources.** Match manifest/lock versions; inspect cloned upstream and nearest usage, never node_modules. Library APIs do not define coding policy.
8
+ **Dependency APIs.** Match manifest/lock versions. Inspect the relevant cloned upstream implementation, types and nearest real usage before inventing an API wrapper or workaround; never node_modules. Follow only the paths needed to settle the question, with no dependency install/build just to read source. Library APIs do not define coding policy.
9
9
 
10
10
  **Diagnostics.** Use the shown idiom, not casts, weakened rules or a rewrite trading one diagnostic for another.
11
11
 
@@ -13,16 +13,18 @@ description: 'Engineering rules for product code, paired with the shared lint. U
13
13
 
14
14
  **Clarity.** Plain, explicit, idiomatic code, names and files; read top to bottom without commentary or cleverness.
15
15
 
16
- - **Requested only.** Build the smallest thing that does the requested job well and touch only what the request needs; an unrelated improvement is listed for the user. A refactor or cleanup request asks for depth instead: every rule applies to every line of the owned files.
16
+ **Experiments.** Write with these idioms; reserve the full refactor/cleanup pass for retained delivery. Keep disposable source isolated and shared rules intact.
17
+
18
+ - **Requested only.** Check whether existing code, an installed module or a platform capability already satisfies the contract before writing more. Implement only the missing behavior; shorter code never justifies dropping requested behavior, safety or accessibility. Touch only what the request needs; unrelated improvements are reported. A refactor or cleanup request asks for depth instead: every rule applies to every line of the owned files.
17
19
  - **Inward layers.** Domain and service code never import HTTP, RPC, or other transport types.
18
- - **Building blocks.** Offer services, Layers, and functions the caller composes like any Effect module, never a wrapper that bundles them behind one call such as `serve(app)`.
20
+ - **Building blocks.** For a consequential new interface, start with a realistic caller operation and its ordering, failure and lifetime obligations; hide useful complexity, not forwarded names. Offer services, Layers and functions the caller composes like any Effect module, never a bundle such as `serve(app)`. No mandatory architecture document or alternatives round.
19
21
  - **No machinery.** Behave correctly instead of building hooks, guards, or generators to enforce behavior.
20
22
  - **Behavior kept.** A refactor keeps behavior at every usage point. Two differences are accepted and reported with the change: one no usage point observes that makes the code simpler, and one a rule here causes, such as an error keeping its cause or sorted keys, unless a usage point parses it; printed diagnostics are not a contract. Any other behavior change is the user's decision, except a reachable bug's fix.
21
- - **Reachable bugs.** A bug a real input from a usage point triggers is fixed and reported, with every consumer of the changed output checked. Handling for input no caller produces is deleted; anything that looks intentional or that other code relies on is kept and reported as possibly intentional.
22
- - **Performance.** Improve performance in the touched code where you know how; measure on a realistic input only when a change claims speed or keeps a slower-looking form. Diagnose at the layer that owns the symptom: distributed spans for RPC/backend dependencies, React commits for rerenders, a browser performance profile for main-thread work. Correlate one real action before changing code, then repeat it on the fix; neither a screenshot nor a service name proves latency or trace coverage. Use the repository's instrumentation and the tool's help; profiling is not a ritual on every change.
23
+ - **Reachable bugs.** A bug a real input from a usage point triggers is fixed and reported, with every consumer of the changed output checked. For hard bugs, use one symptom-specific replay and a prediction per probe; compare known-good/broken states when useful. Remove edits motivated by disproven hypotheses and check the original scenario after reducing a reproduction. Handling for input no caller produces is deleted; anything that looks intentional or that other code relies on is kept and reported as possibly intentional.
24
+ - **Performance.** Improve performance in the touched code where you know how; measure on a realistic input only when a change claims speed or keeps a slower-looking form. Diagnose at the layer that owns the symptom: distributed spans for RPC/backend dependencies, React commits for rerenders, a browser performance profile for main-thread work. Correlate one real action before changing code, then repeat it on the fix; neither a screenshot nor a service name proves latency or trace coverage. Time correct completed work under equivalent workloads/configuration; distinguish setup/cache/profiling cost and report run count/variation. Differences within noise are unproved, not speedups. Use existing instrumentation; profiling is not a ritual on every change.
23
25
  - **Type safety.** A fix never weakens types to make a symptom go away: no widened or erased type, cast, dropped generic, or loosened exported type.
24
26
  - **Domain.** Implement the definition the domain uses, such as a cycle for recursion, never the nearest syntactic proxy.
25
- - **Reread.** Before finishing, reread the diff and delete every line the outcome does not require.
27
+ - **Reread.** Before delivering retained code, reread the diff and delete every line the outcome does not require.
26
28
 
27
29
  **Happy path.** Failures flow through the error channel: no catch, retry, fallback, backup, or defensive check unless the request or an existing contract needs it.
28
30
 
@@ -52,7 +54,7 @@ Math.floor(Duration.toDays(DateTime.distance(oldest.timestamp, newest.timestamp)
52
54
  import {glob} from 'glob' // a dependency Effect's FileSystem replaces
53
55
  ```
54
56
 
55
- **Whole values.** Pass whole values or spread them. No parameter/callback/loop destructuring except useState and component ref; project fields only to exclude data the recipient must not receive.
57
+ **Whole values.** Pass whole values or spread them. No parameter/callback/loop destructuring except labeled tuple parameters, useState and component ref; project fields only to exclude data the recipient must not receive.
56
58
 
57
59
  ```ts
58
60
  // good
@@ -176,7 +178,7 @@ const parsed = Option.getOrUndefined(Number.parse(event.target.value))
176
178
 
177
179
  **Handlers.** A handler used once is an arrow in place; a handler used twice is a `function`.
178
180
 
179
- **Pairs.** A tuple stays whole, read by index.
181
+ **Pairs.** Positional tuples stay whole, read by index. Meaningful named bindings such as React state/setter pairs and explicitly labeled tuple parameters are useful exceptions.
180
182
 
181
183
  **Accumulators.** No `let`: a fold or an Effect collection operation that says what it selects.
182
184
 
@@ -223,7 +225,7 @@ process.env.HOST ?? '0.0.0.0'
223
225
 
224
226
  **Pipe.** A standalone `pipe`, nested at most three deep; never the `.pipe` method.
225
227
 
226
- **Failures.** In a generator a failure is `return yield* E.make(...)`; outside one, `Effect.fail`.
228
+ **Failures.** Yield a yieldable domain error directly in a generator: `return yield* E.make(...)`. Keep `Effect.fail(error)` when the error is not yieldable; outside a generator, use `Effect.fail`. Do not infer yieldability from factory-call syntax.
227
229
 
228
230
  ```ts
229
231
  // good
@@ -240,7 +242,7 @@ const text = Effect.fn('Ai.text')(function* (response: AiResponse) {
240
242
  })
241
243
  ```
242
244
 
243
- **Casts.** No `as`, `any`, or `unknown` annotation: decode unknown data once with a schema at the boundary; `satisfies` and `as const` are the only assertions.
245
+ **Casts.** No `as`, `any` or erasing a known shape into `unknown`; `satisfies` and `as const` are the only assertions. Genuinely opaque SDK/framework inputs remain `unknown` until narrowed or decoded at their boundary—do not fabricate a shape to avoid that type. Decode incoming protocol data once with Schema before use.
244
246
 
245
247
  ```ts
246
248
  // good
@@ -248,7 +250,7 @@ const params = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Un
248
250
  return Effect.succeed([{text: part.text, type: 'text'} satisfies TextContent])
249
251
  // bad — a cast satisfies one rule and the next flags it
250
252
  const params = part.params as Record<string, unknown>
251
- create: Effect.fnUntraced(function* (input: unknown) {
253
+ const params: unknown = part.params // erases an already-known shape
252
254
  ```
253
255
 
254
256
  **Decoders.** `Schema.decodeUnknownEffect` only for `unknown` input; typed input uses `Schema.decodeEffect`, and JSON text goes through `Schema.fromJsonString`.
@@ -279,7 +281,7 @@ import {type Plugin, createServer} from 'vite'
279
281
  export const EntryKind = Schema.Literals(['income', 'expense']) // no importer
280
282
  ```
281
283
 
282
- **Readonly.** Every property of a service shape is readonly, as in Effect's services, and so may class members be; readonly appears nowhere else, except on a `Schema.suspend` cycle's hand-written types.
284
+ **Readonly.** Required on service-shape properties. Allowed on class members and handwritten `Schema.suspend` cycle types; otherwise forbidden.
283
285
 
284
286
  **Disables.** Fix the idiom or reproduce a rule defect. Inline disables require an unavoidable conflict and -- reason; never disable ordering or weaken file configuration. Report dependency-only diagnostics; rename filename violations.
285
287
 
@@ -377,7 +379,7 @@ static readonly layer = Layer.effect(Ledger, makeLedger) // a make with one call
377
379
  query(sql: string): Effect.Effect<Rows> // a method signature instead of a readonly property
378
380
  ```
379
381
 
380
- **Primitives.** Use Effect's primitives directly; rpcs and services already trace, and one scope owns each lifetime.
382
+ **Primitives.** Use Effect's primitives directly; rpcs and services already trace, and one scope owns each lifetime. Compose established independent I/O with Effect.all and explicit appropriate concurrency; keep order-dependent, transactional or resource-conflicting work sequential.
381
383
 
382
384
  ```ts
383
385
  // good
@@ -442,7 +444,7 @@ const label = MutableRef.make<string>('') // repeats the inferred type
442
444
  export type SandboxLike = {id: string; renew: (seconds: number) => Promise<unknown>} // a copy of an inferred shape
443
445
  ```
444
446
 
445
- **Schema pairs.** Type pair immediately before every schema. Structs use Schema.Struct, never Schema.Class; reuse fields by spreading. Call schema.make directly for validation/defaults.
447
+ **Schema pairs.** Type pair immediately before every schema. Structs use Schema.Struct, never Schema.Class; reuse fields by spreading. Use Schema.optionalKey for absent fields; Schema.optional allows explicit undefined too, so reserve it for that contract.
446
448
 
447
449
  ```ts
448
450
  // good
@@ -460,6 +462,18 @@ function cursorFromBigInt(value: bigint) {
460
462
  payload: Schema.Struct(pipe(PortfolioVisitor.fields, Struct.pick(['color', 'id', 'name', 'x', 'y']))) // the schema itself fits
461
463
  ```
462
464
 
465
+ **Construction.** `schema.make` applies defaults and validates synchronously, throwing on failure; use it for trusted construction. For reachable fallible construction with defaults, use `schema.makeEffect` and map its `SchemaIssue.Issue` to the owning domain error. Effect decoders report `SchemaError`; use the correct error contract and do not revalidate already-narrowed data.
466
+
467
+ ```ts
468
+ // good — reachable constructor failure stays in the error channel
469
+ pipe(
470
+ Name.makeEffect(input),
471
+ Effect.mapError(cause => NameError.make({cause, message: 'Invalid name'}))
472
+ )
473
+ // bad — input can fail a refinement even though its TypeScript type is string
474
+ Name.make(input)
475
+ ```
476
+
463
477
  **Cycles.** Handwrite each recursive-cycle type; annotate only Schema.suspend thunks. Noncycle types stay inferred; unions follow members.
464
478
 
465
479
  ```ts
@@ -557,6 +571,6 @@ const state = useState(false)
557
571
  ```
558
572
 
559
573
  - **Pure render.** Derive display values from props and subscribed state during render; never mutate them or perform an RPC, write or subscription there. Event handlers own user-triggered actions; Effects synchronize external systems, not copied state or calculations. Reuse the existing router/atom integration rather than fetching in a new Effect.
560
- - **State identity.** Keep one owner for each value, stable domain keys for list items and deliberate reset boundaries. Compiler memoization is a performance optimization, never a correctness or lifetime guarantee. Do not add manual memoization, compiler escape directives or weakened hook rules to hide an ownership problem.
574
+ - **State identity.** Keep one owner for each value, stable domain keys for list items and deliberate reset boundaries. For an observed rerender problem, derive the needed value in the existing atom graph instead of subscribing to frequently changing unrelated state. Compiler memoization is a performance optimization, never a correctness or lifetime guarantee. Do not add manual memoization, compiler escape directives or weakened hook rules to hide an ownership problem.
561
575
 
562
576
  Testing judgment, fixtures, assertions and verification belong to the testing skill; these engineering rules still apply to test code.
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  name: testing
3
- description: 'Curated black-box tests and trustworthy verification for Effect applications and tools. Read before writing, reviewing or running tests; apply engineering to the test code.'
3
+ description: 'Select and run black-box behavior proof for Effect apps/tools. Use when choosing, writing, reviewing or running tests; apply engineering to test code and keep exploratory proof question-sized.'
4
4
  ---
5
5
 
6
6
  # Testing
7
7
 
8
- **Scope.** Guard owned behavior, not coverage. CLI-installed skills are read-only; change their source and refresh via CLI.
8
+ **Scope.** Guard owned behavior, not coverage.
9
+
10
+ **Timing.** An experiment needs trustworthy proof of its question, not comprehensive release coverage. Final acceptance applies to retained delivery; label stubbed/unproved behavior.
9
11
 
10
12
  - **Worth it.** Test owned branching, computation, parsing/state transitions a plausible regression breaks; add missing valuable coverage before refactoring.
11
13
  - **Seam.** Test the exported layer, service, or function of a package or an app's service as a black box, so rewriting the implementation leaves every test green; the browser proves UI components.
@@ -38,7 +40,7 @@ assert.strictEqual(Array.flatMap(attioProvider.groups, group => group.actions).l
38
40
 
39
41
  ## Choose cases
40
42
 
41
- - **Contract.** Reachable input → observable output; a plausible wrong implementation must fail. Use the nearest public seam, not private helpers/new harnesses.
43
+ - **Contract.** Reachable input → observable output; a plausible wrong implementation must fail. Use the nearest public seam, not private helpers/new harnesses. When behavior is uncertain, settle one reachable case with a focused check and minimal implementation, then choose the next case from the findings; do not batch tests for an imagined implementation or require universal TDD.
42
44
  - **Branches.** Exercise reachable empty/duplicate/concurrent/dependency-failure distinctions; no Cartesian product or random/repeated happy paths without a hypothesis.
43
45
  - **Failure.** Effect.flip for expected failure; Effect.exit for both outcomes. Assert domain identity and preserved state/resources: no partial write, lost value, extra retry or leak. Wording/rejection alone is insufficient; no incidental order assertions.
44
46
  - **Control.** Run against the unfixed implementation: the intended assertion fails, not setup/import/compilation. For refactoring, add missing coverage on old behavior first. Keep durable regressions, remove settled probes.
@@ -60,4 +62,4 @@ assert.strictEqual(Array.flatMap(attioProvider.groups, group => group.actions).l
60
62
  - **Inventory.** Collect independent failures once instead of cancelling the suite at the first failure. Distinguish separate cases from repeated observations. A timeout receives one isolated rerun of that same test; after a genuine pass, stop investigating it. Do not call a bypassed selector or a skipped operation flaky.
61
63
  - **Evidence.** Preserve the first failure, exact command, execution count and exit status. Separate cause from hypothesis, product defect from setup failure, and a skipped criterion from a passing one. A source read or overall green command never substitutes for a missing behavior check.
62
64
  - **Stable inputs.** No writer changes a running check's source, dependencies, configuration or build inputs. A relevant edit invalidates its prior result; rerun affected criteria and reuse unaffected evidence. Independent research or browser journeys may continue without mutating those inputs.
63
- - **Final proof.** Reconcile every required behavior on the final code. Screenshots/video prove visible outcomes; public-seam execution proves nonvisual behavior; fresh agents prove changed agent instructions. Keep the result and useful evidence, not disposable drivers, fixture installs, reports or preview services.
65
+ - **Final proof.** Reconcile every required behavior on the final code. Screenshots/video prove visible outcomes; public-seam execution proves nonvisual behavior; use fresh agents for changed instructions only when they can settle a consequential unresolved uncertainty, not routine wording edits. Keep the result and useful evidence, not disposable drivers, fixture installs, reports or preview services.